From a84a22896739ff0966150623c4cbf59c6f2578de Mon Sep 17 00:00:00 2001 From: "yuanyi.lp" Date: Fri, 31 Dec 2021 17:03:04 +0800 Subject: [PATCH] feature(ingress): update the annotation process --- docs/usage-alb.md | 314 ++++++------- .../reconcile/annotations/annotations.go | 398 +++++++++++----- .../annotations/class_annotation_matcher.go | 40 -- .../ingress/reconcile/annotations/parser.go | 426 ------------------ 4 files changed, 443 insertions(+), 735 deletions(-) delete mode 100644 pkg/controller/ingress/reconcile/annotations/class_annotation_matcher.go delete mode 100644 pkg/controller/ingress/reconcile/annotations/parser.go diff --git a/docs/usage-alb.md b/docs/usage-alb.md index ec4718ef0..ab686be00 100644 --- a/docs/usage-alb.md +++ b/docs/usage-alb.md @@ -52,7 +52,7 @@ When you create an Ingress, an Albconfig object named default is automatically c ```bash kubectl -n kube-system get albconfig ``` - output: + 预期输出: ```bash NAME AGE default 87m @@ -274,6 +274,7 @@ Step 2: Configure an Ingress serviceName: coffee-svc servicePort: 80 ``` + The following table describes the parameters in the YAML template. @@ -340,111 +341,111 @@ An Ingress is an API object that you can use to provide Layer 7 load balancing t Perform the following steps to create a simple Ingress with or without a domain name to forward requests. - Create a simple Ingress with a domain name. 1. Use the following template to create a Deployment, a Service, and an Ingress. Requests to the domain name of the Ingress are forwarded to the Service. - ``` - apiVersion: v1 - kind: Service - metadata: - name: demo-service - namespace: default - spec: - ports: - - name: port1 - port: 80 - protocol: TCP - targetPort: 8080 - selector: - app: demo - sessionAffinity: None - type: ClusterIP - - --- - apiVersion: apps/v1 - kind: Deployment - metadata: - name: demo - namespace: default - spec: - replicas: 1 - selector: - matchLabels: - app: demo - template: - metadata: - labels: - app: demo - spec: - containers: - - image: registry.cn-hangzhou.aliyuncs.com/alb-sample/cafe:v1 - imagePullPolicy: IfNotPresent - name: demo - ports: - - containerPort: 8080 - protocol: TCP - --- - apiVersion: networking.k8s.io/v1beta1 - kind: Ingress - metadata: - annotations: - alb.ingress.kubernetes.io/address-type: internet - alb.ingress.kubernetes.io/vswitch-ids: "vsw-2zeqgkyib34gw1fxs****,vsw-2zefv5qwao4przzlo****" - kubernetes.io/ingress.class: alb - name: demo - namespace: default - spec: - rules: - - host: demo.domain.ingress.top # The domain name of the Ingress. - http: - paths: - - backend: - serviceName: demo-service - servicePort: 80 - path: /hello - pathType: ImplementationSpecific - ``` + ``` + apiVersion: v1 + kind: Service + metadata: + name: demo-service + namespace: default + spec: + ports: + - name: port1 + port: 80 + protocol: TCP + targetPort: 8080 + selector: + app: demo + sessionAffinity: None + type: ClusterIP + + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: demo + namespace: default + spec: + replicas: 1 + selector: + matchLabels: + app: demo + template: + metadata: + labels: + app: demo + spec: + containers: + - image: registry.cn-hangzhou.aliyuncs.com/alb-sample/cafe:v1 + imagePullPolicy: IfNotPresent + name: demo + ports: + - containerPort: 8080 + protocol: TCP + --- + apiVersion: networking.k8s.io/v1beta1 + kind: Ingress + metadata: + annotations: + alb.ingress.kubernetes.io/address-type: internet + alb.ingress.kubernetes.io/vswitch-ids: "vsw-2zeqgkyib34gw1fxs****,vsw-2zefv5qwao4przzlo****" + kubernetes.io/ingress.class: alb + name: demo + namespace: default + spec: + rules: + - host: demo.domain.ingress.top # The domain name of the Ingress. + http: + paths: + - backend: + serviceName: demo-service + servicePort: 80 + path: /hello + pathType: ImplementationSpecific + ``` 2. Run the following command to access the application by using the specified domain name. Replace ADDRESS with the address of the related ALB instance. You can obtain the address by running the kubectl get ing command. - ```bash - curl -H "host: demo.domain.ingress.top"
/hello - ``` + ```bash + curl -H "host: demo.domain.ingress.top"
/hello + ``` Expected output: - ```bash - {"hello":"coffee"} - ``` + ```bash + {"hello":"coffee"} + ``` - Create a simple Ingress without a domain name. 1. Use the following template to create an Ingress: - ```yaml - apiVersion: networking.k8s.io/v1beta1 - kind: Ingress - metadata: - annotations: - alb.ingress.kubernetes.io/address-type: internet - alb.ingress.kubernetes.io/vswitch-ids: "vsw-2zeqgkyib34gw1fxs****,vsw-2zefv5qwao4przzlo****" - kubernetes.io/ingress.class: alb - name: demo - namespace: default - spec: - rules: - - host: "" - http: - paths: - - backend: - serviceName: demo-service - servicePort: 80 - path: /hello - pathType: ImplementationSpecific - ``` + ```yaml + apiVersion: networking.k8s.io/v1beta1 + kind: Ingress + metadata: + annotations: + alb.ingress.kubernetes.io/address-type: internet + alb.ingress.kubernetes.io/vswitch-ids: "vsw-2zeqgkyib34gw1fxs****,vsw-2zefv5qwao4przzlo****" + kubernetes.io/ingress.class: alb + name: demo + namespace: default + spec: + rules: + - host: "" + http: + paths: + - backend: + serviceName: demo-service + servicePort: 80 + path: /hello + pathType: ImplementationSpecific + ``` 2. Run the following command to access the application without using a domain name. Replace ADDRESS with the address of the related ALB instance. You can obtain the address by running the kubectl get ing command. - ```bash - curl
/hello - ``` + ```bash + curl
/hello + ``` Expected output: - ```bash - {"hello":"coffee"} - ``` + ```bash + {"hello":"coffee"} + ``` ### Forward requests based on URL paths @@ -454,25 +455,25 @@ The following steps show how to configure different URL match policies. - Exact:exactly matches the URL path with case sensitivity. 1. Use the following template to create an Ingress: - ```yaml - apiVersion: networking.k8s.io/v1beta1 - kind: Ingress - metadata: - annotations: - alb.ingress.kubernetes.io/vswitch-ids: "vsw-2zeqgkyib34gw1fxs****,vsw-2zefv5qwao4przzlo****" - kubernetes.io/ingress.class: alb - name: demo-path - namespace: default - spec: - rules: - - http: - paths: - - path: /hello - backend: - serviceName: demo-service - servicePort: 80 - pathType: Exact - ``` + ```yaml + apiVersion: networking.k8s.io/v1beta1 + kind: Ingress + metadata: + annotations: + alb.ingress.kubernetes.io/vswitch-ids: "vsw-2zeqgkyib34gw1fxs****,vsw-2zefv5qwao4przzlo****" + kubernetes.io/ingress.class: alb + name: demo-path + namespace: default + spec: + rules: + - http: + paths: + - path: /hello + backend: + serviceName: demo-service + servicePort: 80 + pathType: Exact + ``` 2. Run the following command to access the application. Replace ADDRESS with the address of the related ALB instance. You can obtain the address by running the kubectl get ing command. @@ -486,26 +487,26 @@ The following steps show how to configure different URL match policies. - ImplementationSpecific: the default match policy. For ALB Ingresses, the ImplementationSpecific policy has the same effect as the Exact policy. However, the controllers of Ingresses with the ImplementationSpecific policy and the controllers Ingresses with the Exact policy are implemented in different ways. 1. Use the following template to create an Ingress: - ```yaml - apiVersion: networking.k8s.io/v1beta1 - kind: Ingress - metadata: - annotations: - alb.ingress.kubernetes.io/address-type: internet - alb.ingress.kubernetes.io/vswitch-ids: "vsw-2zeqgkyib34gw1fxs****,vsw-2zefv5qwao4przzlo****" - kubernetes.io/ingress.class: alb - name: demo-path - namespace: default - spec: - rules: - - http: - paths: - - path: /hello - backend: - serviceName: demo-service - servicePort: 80 - pathType: ImplementationSpecific - ``` + ```yaml + apiVersion: networking.k8s.io/v1beta1 + kind: Ingress + metadata: + annotations: + alb.ingress.kubernetes.io/address-type: internet + alb.ingress.kubernetes.io/vswitch-ids: "vsw-2zeqgkyib34gw1fxs****,vsw-2zefv5qwao4przzlo****" + kubernetes.io/ingress.class: alb + name: demo-path + namespace: default + spec: + rules: + - http: + paths: + - path: /hello + backend: + serviceName: demo-service + servicePort: 80 + pathType: ImplementationSpecific + ``` 2. Run the following command to access the application. Replace ADDRESS with the address of the related ALB instance. You can obtain the address by running the kubectl get ing command. @@ -519,26 +520,26 @@ The following steps show how to configure different URL match policies. - Prefix: matches based on a URL path prefix separated by forward slashes (/). The match is case-sensitive and performed on each element of the path. 1. Use the following template to create an Ingress: - ```yaml - apiVersion: networking.k8s.io/v1beta1 - kind: Ingress - metadata: - annotations: - alb.ingress.kubernetes.io/address-type: internet - alb.ingress.kubernetes.io/vswitch-ids: "vsw-2zeqgkyib34gw1fxs****,vsw-2zefv5qwao4przzlo****" - kubernetes.io/ingress.class: alb - name: demo-path-prefix - namespace: default - spec: - rules: - - http: - paths: - - path: / - backend: - serviceName: demo-service - servicePort: 80 - pathType: Prefix - ``` + ```yaml + apiVersion: networking.k8s.io/v1beta1 + kind: Ingress + metadata: + annotations: + alb.ingress.kubernetes.io/address-type: internet + alb.ingress.kubernetes.io/vswitch-ids: "vsw-2zeqgkyib34gw1fxs****,vsw-2zefv5qwao4przzlo****" + kubernetes.io/ingress.class: alb + name: demo-path-prefix + namespace: default + spec: + rules: + - http: + paths: + - path: / + backend: + serviceName: demo-service + servicePort: 80 + pathType: Prefix + ``` 2. Run the following command to access the application. Replace ADDRESS with the address of the related ALB instance. You can obtain the address by running the kubectl get ing command. @@ -678,7 +679,7 @@ The following table describes the parameters in the YAML template. The ALB Ingress controller supports automatic certificate discovery. You must first create a certificate in the SSL Certificates console. Then, specify the domain name of the certificate in the Transport Layer Security (TLS) configurations of the Ingress. This way, the ALB Ingress controller can automatically match and discover the certificate based on the TLS configurations of the Ingress. -1. Configure automatic certificate discovery +1. Run the following openssl commands to create a certificate: ```bash openssl genrsa -out albtop-key.pem 4096 openssl req -subj "/CN=demo.alb.ingress.top" -sha256 -new -key albtop-key.pem -out albtop.csr @@ -951,4 +952,5 @@ metadata:
-
\ No newline at end of file + + diff --git a/pkg/controller/ingress/reconcile/annotations/annotations.go b/pkg/controller/ingress/reconcile/annotations/annotations.go index e408eac46..217c76bbe 100644 --- a/pkg/controller/ingress/reconcile/annotations/annotations.go +++ b/pkg/controller/ingress/reconcile/annotations/annotations.go @@ -1,37 +1,19 @@ package annotations import ( + "encoding/json" "fmt" + "strconv" "strings" - corev1 "k8s.io/api/core/v1" - "k8s.io/klog" + "github.com/pkg/errors" + networking "k8s.io/api/networking/v1" ) // prefix const ( // DefaultAnnotationsPrefix defines the common prefix used in the nginx ingress controller DefaultAnnotationsPrefix = "alb.ingress.kubernetes.io" - - // AnnotationLegacyPrefix legacy prefix of service annotation - AnnotationLegacyPrefix = "service.beta.kubernetes.io/alicloud" - // AnnotationPrefix prefix of service annotation - AnnotationPrefix = "service.beta.kubernetes.io/alibaba-cloud" -) - -const ( - TAGKEY = "kubernetes.do.not.delete" - ACKKEY = "ack.aliyun.com" - PostPay = "PostPay" -) -const ( - INGRESS_ALB_ANNOTATIONS = "alb.ingress.kubernetes.io/lb" - // config the rule conditions - INGRESS_ALB_CONDITIONS_ANNOTATIONS = "alb.ingress.kubernetes.io/conditions.%s" - // config the rule actions - INGRESS_ALB_ACTIONS_ANNOTATIONS = "alb.ingress.kubernetes.io/actions.%s" - // config the certificate id - INGRESS_ALB_CERTIFICATE_ANNOTATIONS = "alb.ingress.kubernetes.io/certificate-id" ) // load balancer annotations @@ -40,60 +22,29 @@ const ( AnnotationLoadBalancerPrefix = "alb.ingress.kubernetes.io/" // Load Balancer Attribute - AddressType = AnnotationLoadBalancerPrefix + "address-type" // AddressType loadbalancer address type - VswitchIds = AnnotationLoadBalancerPrefix + "vswitch-ids" // VswitchId loadbalancer vswitch id - SLBNetworkType = AnnotationLoadBalancerPrefix + "slb-network-type" // SLBNetworkType loadbalancer network type - ChargeType = AnnotationLoadBalancerPrefix + "charge-type" // ChargeType lb charge type - LoadBalancerId = AnnotationLoadBalancerPrefix + "id" // LoadBalancerId lb id - OverrideListener = AnnotationLoadBalancerPrefix + "force-override-listeners" // OverrideListener force override listeners - LoadBalancerName = AnnotationLoadBalancerPrefix + "name" // LoadBalancerName slb name - MasterZoneID = AnnotationLoadBalancerPrefix + "master-zoneid" // MasterZoneID master zone id - SlaveZoneID = AnnotationLoadBalancerPrefix + "slave-zoneid" // SlaveZoneID slave zone id - Bandwidth = AnnotationLoadBalancerPrefix + "bandwidth" // Bandwidth bandwidth - AdditionalTags = AnnotationLoadBalancerPrefix + "additional-resource-tags" // AdditionalTags For example: "Key1=Val1,Key2=Val2,KeyNoVal1=,KeyNoVal2",same with aws - Spec = AnnotationLoadBalancerPrefix + "spec" // Spec slb spec - Scheduler = AnnotationLoadBalancerPrefix + "scheduler" // Scheduler slb scheduler - IPVersion = AnnotationLoadBalancerPrefix + "ip-version" // IPVersion ip version - ResourceGroupId = AnnotationLoadBalancerPrefix + "resource-group-id" // ResourceGroupId resource group id - DeleteProtection = AnnotationLoadBalancerPrefix + "delete-protection" // DeleteProtection delete protection - ModificationProtection = AnnotationLoadBalancerPrefix + "modification-protection" // ModificationProtection modification type - ExternalIPType = AnnotationLoadBalancerPrefix + "external-ip-type" // ExternalIPType external ip type - AddressAllocatedMode = AnnotationLoadBalancerPrefix + "address-allocated-mode" - LoadBalancerEdition = AnnotationLoadBalancerPrefix + "edition" - HTTPS = AnnotationLoadBalancerPrefix + "backend-protocol" - ListenPorts = AnnotationLoadBalancerPrefix + "listen-ports" - AccessLog = AnnotationLoadBalancerPrefix + "access-log" - // Listener Attribute - AclStatus = AnnotationLoadBalancerPrefix + "acl-status" // AclStatus enable or disable acl on all listener - AclID = AnnotationLoadBalancerPrefix + "acl-id" // AclID acl id - AclType = AnnotationLoadBalancerPrefix + "acl-type" // AclType acl type, black or white - ProtocolPort = AnnotationLoadBalancerPrefix + "protocol-port" // ProtocolPort protocol port - ForwardPort = AnnotationLoadBalancerPrefix + "forward-port" // ForwardPort loadbalancer forward port - CertID = AnnotationLoadBalancerPrefix + "cert-id" // CertID cert id - HealthCheckEnabled = AnnotationLoadBalancerPrefix + "healthcheck-enabled" // HealthCheckFlag health check flag - HealthCheckPath = AnnotationLoadBalancerPrefix + "healthcheck-path" // HealthCheckType health check type - HealthCheckPort = AnnotationLoadBalancerPrefix + "healthcheck-port" // HealthCheckURI health check uri - HealthCheckProtocol = AnnotationLoadBalancerPrefix + "healthcheck-protocol" // HealthCheckConnectPort health check connect port - HealthCheckMethod = AnnotationLoadBalancerPrefix + "healthcheck-method" // HealthyThreshold health check healthy thresh hold - HealthCheckInterval = AnnotationLoadBalancerPrefix + "healthcheck-interval-seconds" // HealthCheckInterval health check interval - HealthCheckTimeout = AnnotationLoadBalancerPrefix + "healthcheck-timeout-seconds" // HealthCheckTimeout health check timeout - HealthThreshold = AnnotationLoadBalancerPrefix + "healthy-threshold-count" // HealthCheckDomain health check domain - UnHealthThreshold = AnnotationLoadBalancerPrefix + "unhealthy-threshold-count" // HealthCheckHTTPCode health check http code - HealthCheckHTTPCode = AnnotationLoadBalancerPrefix + "healthcheck-httpcode" - SessionStick = AnnotationLoadBalancerPrefix + "sticky-session" // SessionStick sticky session - SessionStickType = AnnotationLoadBalancerPrefix + "sticky-session-type" // SessionStickType session sticky type - CookieTimeout = AnnotationLoadBalancerPrefix + "cookie-timeout" // CookieTimeout cookie timeout - Cookie = AnnotationLoadBalancerPrefix + "cookie" // Cookie lb cookie - PersistenceTimeout = AnnotationLoadBalancerPrefix + "persistence-timeout" // PersistenceTimeout persistence timeout - ConnectionDrain = AnnotationLoadBalancerPrefix + "connection-drain" // ConnectionDrain connection drain - ConnectionDrainTimeout = AnnotationLoadBalancerPrefix + "connection-drain-timeout" // ConnectionDrainTimeout connection drain timeout - PortVGroup = AnnotationLoadBalancerPrefix + "port-vgroup" // VGroupIDs binding user managed vGroup ids to ports - + AddressType = AnnotationLoadBalancerPrefix + "address-type" // AddressType loadbalancer address type + VswitchIds = AnnotationLoadBalancerPrefix + "vswitch-ids" // VswitchId loadbalancer vswitch id + ChargeType = AnnotationLoadBalancerPrefix + "charge-type" // ChargeType lb charge type + LoadBalancerId = AnnotationLoadBalancerPrefix + "id" // LoadBalancerId lb id + OverrideListener = AnnotationLoadBalancerPrefix + "force-override-listeners" // OverrideListener force override listeners + LoadBalancerName = AnnotationLoadBalancerPrefix + "name" // LoadBalancerName slb name + AddressAllocatedMode = AnnotationLoadBalancerPrefix + "address-allocated-mode" + LoadBalancerEdition = AnnotationLoadBalancerPrefix + "edition" + ListenPorts = AnnotationLoadBalancerPrefix + "listen-ports" + AccessLog = AnnotationLoadBalancerPrefix + "access-log" + HealthCheckEnabled = AnnotationLoadBalancerPrefix + "healthcheck-enabled" // HealthCheckFlag health check flag + HealthCheckPath = AnnotationLoadBalancerPrefix + "healthcheck-path" // HealthCheckType health check type + HealthCheckProtocol = AnnotationLoadBalancerPrefix + "healthcheck-protocol" // HealthCheckConnectPort health check connect port + HealthCheckMethod = AnnotationLoadBalancerPrefix + "healthcheck-method" // HealthyThreshold health check healthy thresh hold + HealthCheckInterval = AnnotationLoadBalancerPrefix + "healthcheck-interval-seconds" // HealthCheckInterval health check interval + HealthCheckTimeout = AnnotationLoadBalancerPrefix + "healthcheck-timeout-seconds" // HealthCheckTimeout health check timeout + HealthThreshold = AnnotationLoadBalancerPrefix + "healthy-threshold-count" // HealthCheckDomain health check domain + UnHealthThreshold = AnnotationLoadBalancerPrefix + "unhealthy-threshold-count" // HealthCheckHTTPCode health check http code + HealthCheckHTTPCode = AnnotationLoadBalancerPrefix + "healthcheck-httpcode" // VServerBackend Attribute BackendLabel = AnnotationLoadBalancerPrefix + "backend-label" // BackendLabel backend labels BackendType = "service.beta.kubernetes.io/backend-type" // BackendType backend type RemoveUnscheduled = AnnotationLoadBalancerPrefix + "remove-unscheduled-backend" // RemoveUnscheduled remove unscheduled node from backends - VGroupWeight = AnnotationLoadBalancerPrefix + "vgroup-weight" // VGroupWeight total weight of a vGroup ) const ( @@ -113,72 +64,293 @@ const ( AlbCanaryWeight = AnnotationAlbPrefix + "canary-weight" AlbSslRedirect = AnnotationAlbPrefix + "ssl-redirect" ) -const ( - // ServiceAnnotationPrivateZonePrefix private zone prefix - ServiceAnnotationPrivateZonePrefix = "private-zone-" - // ServiceAnnotationLoadBalancerPrivateZoneName private zone name - PrivateZoneName = ServiceAnnotationPrivateZonePrefix + "name" - // ServiceAnnotationLoadBalancerPrivateZoneId private zone id - PrivateZoneId = ServiceAnnotationPrivateZonePrefix + "id" +type ParseOptions struct { + exact bool + alternativePrefixes []string +} - // ServiceAnnotationLoadBalancerPrivateZoneRecordName private zone record name - PrivateZoneRecordName = ServiceAnnotationPrivateZonePrefix + "record-name" +type ParseOption func(opts *ParseOptions) - // ServiceAnnotationLoadBalancerPrivateZoneRecordTTL private zone record ttl - PrivateZoneRecordTTL = ServiceAnnotationPrivateZonePrefix + "record-ttl" -) +type Parser interface { + ParseStringAnnotation(annotation string, value *string, annotations map[string]string, opts ...ParseOption) bool + + ParseBoolAnnotation(annotation string, value *bool, annotations map[string]string, opts ...ParseOption) (bool, error) -func NewAnnotationRequest(svc *corev1.Service) *AnnotationRequest { - return &AnnotationRequest{svc} + ParseInt64Annotation(annotation string, value *int64, annotations map[string]string, opts ...ParseOption) (bool, error) + + ParseStringSliceAnnotation(annotation string, value *[]string, annotations map[string]string, opts ...ParseOption) bool + + ParseJSONAnnotation(annotation string, value interface{}, annotations map[string]string, opts ...ParseOption) (bool, error) + + ParseStringMapAnnotation(annotation string, value *map[string]string, annotations map[string]string, opts ...ParseOption) (bool, error) } -func (n *AnnotationRequest) Get(k string) string { - if n.Svc == nil { - klog.Infof("extract annotation %s from empty service", k) - return "" +func NewSuffixAnnotationParser(annotationPrefix string) *suffixAnnotationParser { + return &suffixAnnotationParser{ + annotationPrefix: annotationPrefix, } +} + +var _ Parser = (*suffixAnnotationParser)(nil) - if n.Svc.Annotations == nil { - return "" +type suffixAnnotationParser struct { + annotationPrefix string +} + +func (p *suffixAnnotationParser) ParseStringAnnotation(annotation string, value *string, annotations map[string]string, opts ...ParseOption) bool { + ret, _ := p.parseStringAnnotation(annotation, value, annotations, opts...) + return ret +} + +func (p *suffixAnnotationParser) ParseBoolAnnotation(annotation string, value *bool, annotations map[string]string, opts ...ParseOption) (bool, error) { + raw := "" + exists, matchedKey := p.parseStringAnnotation(annotation, &raw, annotations, opts...) + if !exists { + return false, nil + } + val, err := strconv.ParseBool(raw) + if err != nil { + return true, errors.Wrapf(err, "failed to parse bool annotation, %v: %v", matchedKey, raw) } + *value = val + return true, nil +} + +func (p *suffixAnnotationParser) ParseInt64Annotation(annotation string, value *int64, annotations map[string]string, opts ...ParseOption) (bool, error) { + raw := "" + exists, matchedKey := p.parseStringAnnotation(annotation, &raw, annotations, opts...) + if !exists { + return false, nil + } + i, err := strconv.ParseInt(raw, 10, 64) + if err != nil { + return true, errors.Wrapf(err, "failed to parse int64 annotation, %v: %v", matchedKey, raw) + } + *value = i + return true, nil +} - key := composite(AnnotationPrefix, k) - v, ok := n.Svc.Annotations[key] +func (p *suffixAnnotationParser) ParseStringSliceAnnotation(annotation string, value *[]string, annotations map[string]string, opts ...ParseOption) bool { + raw := "" + if exists, _ := p.parseStringAnnotation(annotation, &raw, annotations, opts...); !exists { + return false + } + *value = splitCommaSeparatedString(raw) + return true +} + +func (p *suffixAnnotationParser) ParseJSONAnnotation(annotation string, value interface{}, annotations map[string]string, opts ...ParseOption) (bool, error) { + raw := "" + exists, matchedKey := p.parseStringAnnotation(annotation, &raw, annotations, opts...) + if !exists { + return false, nil + } + if err := json.Unmarshal([]byte(raw), value); err != nil { + return true, errors.Wrapf(err, "failed to parse json annotation, %v: %v", matchedKey, raw) + } + return true, nil +} + +func (p *suffixAnnotationParser) ParseStringMapAnnotation(annotation string, value *map[string]string, annotations map[string]string, opts ...ParseOption) (bool, error) { + raw := "" + exists, matchedKey := p.parseStringAnnotation(annotation, &raw, annotations, opts...) + if !exists { + return false, nil + } + rawKVPairs := splitCommaSeparatedString(raw) + keyValues := make(map[string]string) + for _, kvPair := range rawKVPairs { + parts := strings.SplitN(kvPair, "=", 2) + if len(parts) != 2 { + return false, errors.Errorf("failed to parse stringMap annotation, %v: %v", matchedKey, raw) + } + key := parts[0] + value := parts[1] + if len(key) == 0 { + return false, errors.Errorf("failed to parse stringMap annotation, %v: %v", matchedKey, raw) + } + keyValues[key] = value + } + if value != nil { + *value = keyValues + } + return true, nil +} + +func (p *suffixAnnotationParser) parseStringAnnotation(annotation string, value *string, annotations map[string]string, opts ...ParseOption) (bool, string) { + keys := p.buildAnnotationKeys(annotation, opts...) + for _, key := range keys { + if raw, ok := annotations[key]; ok { + *value = raw + return true, key + } + } + return false, "" +} + +// buildAnnotationKey returns list of full annotation keys based on suffix and parse options +func (p *suffixAnnotationParser) buildAnnotationKeys(suffix string, opts ...ParseOption) []string { + keys := []string{} + parseOpts := ParseOptions{} + for _, opt := range opts { + opt(&parseOpts) + } + if parseOpts.exact { + keys = append(keys, suffix) + } else { + keys = append(keys, fmt.Sprintf("%v/%v", p.annotationPrefix, suffix)) + for _, pfx := range parseOpts.alternativePrefixes { + keys = append(keys, fmt.Sprintf("%v/%v", pfx, suffix)) + } + } + return keys +} + +func splitCommaSeparatedString(commaSeparatedString string) []string { + var result []string + parts := strings.Split(commaSeparatedString, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + if len(part) == 0 { + continue + } + result = append(result, part) + } + return result +} + +// IngressAnnotation has a method to parse annotations located in Ingress +type IngressAnnotation interface { + Parse(ing *networking.Ingress) (interface{}, error) +} + +type ingAnnotations map[string]string + +func (a ingAnnotations) parseBool(name string) (bool, error) { + val, ok := a[name] if ok { - return v + b, err := strconv.ParseBool(val) + if err != nil { + return false, NewInvalidAnnotationContent(name, val) + } + return b, nil } + return false, ErrMissingAnnotations +} - lkey := composite(AnnotationLegacyPrefix, k) - v, ok = n.Svc.Annotations[lkey] +func (a ingAnnotations) parseString(name string) (string, error) { + val, ok := a[name] if ok { - return v + s := normalizeString(val) + if len(s) == 0 { + return "", NewInvalidAnnotationContent(name, val) + } + + return s, nil + } + return "", ErrMissingAnnotations +} + +func (a ingAnnotations) parseInt(name string) (int, error) { + val, ok := a[name] + if ok { + i, err := strconv.Atoi(val) + if err != nil { + return 0, NewInvalidAnnotationContent(name, val) + } + return i, nil + } + return 0, ErrMissingAnnotations +} + +func checkAnnotation(name string, ing *networking.Ingress) error { + if ing == nil || len(ing.GetAnnotations()) == 0 { + return ErrMissingAnnotations + } + if name == "" { + return ErrInvalidAnnotationName + } + + return nil +} + +// GetStringAnnotation extracts a string from an Ingress annotation +func GetStringAnnotation(name string, ing *networking.Ingress) (string, error) { + v := GetAnnotationWith(name) + err := checkAnnotation(v, ing) + if err != nil { + return "", err } + return ingAnnotations(ing.GetAnnotations()).parseString(v) +} + +// GetStringAnnotationMutil extracts a string from an Ingress annotation +func GetStringAnnotationMutil(name, name1 string, ing *networking.Ingress) string { + if val, ok := ing.Annotations[name]; ok { + return val + } + if val, ok := ing.Annotations[name1]; ok { + return val + } return "" } -func (n *AnnotationRequest) GetRaw(k string) string { - return n.Svc.Annotations[k] +// GetAnnotationWith returns the ingress annotations +func GetAnnotationWith(ann string) string { + return fmt.Sprintf("%v", ann) } -func (n *AnnotationRequest) GetDefaultLoadBalancerName() string { - //GCE requires that the name of a load balancer starts with a lower case letter. - ret := "a" + string(n.Svc.UID) - ret = strings.Replace(ret, "-", "", -1) - //AWS requires that the name of a load balancer is shorter than 32 bytes. - if len(ret) > 32 { - ret = ret[:32] +func normalizeString(input string) string { + trimmedContent := []string{} + for _, line := range strings.Split(input, "\n") { + trimmedContent = append(trimmedContent, strings.TrimSpace(line)) } - return ret + + return strings.Join(trimmedContent, "\n") } -func composite(p, k string) string { - return fmt.Sprintf("%s-%s", p, k) +var ( + // ErrMissingAnnotations the ingress rule does not contain annotations + // This is an error only when annotations are being parsed + ErrMissingAnnotations = errors.New("ingress rule without annotations") + + // ErrInvalidAnnotationName the ingress rule does contains an invalid + // annotation name + ErrInvalidAnnotationName = errors.New("invalid annotation name") +) + +// NewInvalidAnnotationContent returns a new InvalidContent error +func NewInvalidAnnotationContent(name string, val interface{}) error { + return InvalidContent{ + Name: fmt.Sprintf("the annotation %v does not contain a valid value (%v)", name, val), + } +} + +// InvalidConfiguration Error +type InvalidConfiguration struct { + Name string +} + +func (e InvalidConfiguration) Error() string { + return e.Name } -func Annotation(k string) string { - return composite(AnnotationPrefix, k) +// InvalidContent error +type InvalidContent struct { + Name string } -type AnnotationRequest struct{ Svc *corev1.Service } +func (e InvalidContent) Error() string { + return e.Name +} + +// LocationDenied error +type LocationDenied struct { + Reason error +} + +func (e LocationDenied) Error() string { + return e.Reason.Error() +} diff --git a/pkg/controller/ingress/reconcile/annotations/class_annotation_matcher.go b/pkg/controller/ingress/reconcile/annotations/class_annotation_matcher.go deleted file mode 100644 index cacd37e5b..000000000 --- a/pkg/controller/ingress/reconcile/annotations/class_annotation_matcher.go +++ /dev/null @@ -1,40 +0,0 @@ -package annotations - -import ( - networking "k8s.io/api/networking/v1" - "k8s.io/cloud-provider-alibaba-cloud/pkg/util" -) - -type ClassAnnotationMatcher interface { - Matches(ingClassAnnotation string) bool -} - -func NewDefaultClassAnnotationMatcher(ingressClass string) *defaultClassAnnotationMatcher { - return &defaultClassAnnotationMatcher{ - ingressClass: ingressClass, - } -} - -var _ ClassAnnotationMatcher = &defaultClassAnnotationMatcher{} - -type defaultClassAnnotationMatcher struct { - ingressClass string -} - -func (m *defaultClassAnnotationMatcher) Matches(ingClassAnnotation string) bool { - if m.ingressClass == "" && ingClassAnnotation == util.IngressClassALB { - return true - } - return ingClassAnnotation == m.ingressClass -} - -func IsIngressAlbClass(ing networking.Ingress) bool { - classAnnotationMatcher := NewDefaultClassAnnotationMatcher(util.IngressClassALB) - - if ingClassAnnotation, exists := ing.Annotations[util.IngressClass]; exists { - if matchesIngressClass := classAnnotationMatcher.Matches(ingClassAnnotation); matchesIngressClass { - return true - } - } - return false -} diff --git a/pkg/controller/ingress/reconcile/annotations/parser.go b/pkg/controller/ingress/reconcile/annotations/parser.go deleted file mode 100644 index 84778aaaf..000000000 --- a/pkg/controller/ingress/reconcile/annotations/parser.go +++ /dev/null @@ -1,426 +0,0 @@ -package annotations - -import ( - "encoding/json" - "fmt" - "net/url" - "strconv" - "strings" - - "github.com/pkg/errors" - networking "k8s.io/api/networking/v1" - "k8s.io/apimachinery/pkg/util/sets" -) - -type ParseOptions struct { - exact bool - alternativePrefixes []string -} - -type ParseOption func(opts *ParseOptions) - -func WithExact() ParseOption { - return func(opts *ParseOptions) { - opts.exact = true - } -} - -func WithAlternativePrefixes(prefixes ...string) ParseOption { - return func(opts *ParseOptions) { - opts.alternativePrefixes = append(opts.alternativePrefixes, prefixes...) - } -} - -type Parser interface { - ParseStringAnnotation(annotation string, value *string, annotations map[string]string, opts ...ParseOption) bool - - ParseBoolAnnotation(annotation string, value *bool, annotations map[string]string, opts ...ParseOption) (bool, error) - - ParseInt64Annotation(annotation string, value *int64, annotations map[string]string, opts ...ParseOption) (bool, error) - - ParseStringSliceAnnotation(annotation string, value *[]string, annotations map[string]string, opts ...ParseOption) bool - - ParseJSONAnnotation(annotation string, value interface{}, annotations map[string]string, opts ...ParseOption) (bool, error) - - ParseStringMapAnnotation(annotation string, value *map[string]string, annotations map[string]string, opts ...ParseOption) (bool, error) -} - -func NewSuffixAnnotationParser(annotationPrefix string) *suffixAnnotationParser { - return &suffixAnnotationParser{ - annotationPrefix: annotationPrefix, - } -} - -var _ Parser = (*suffixAnnotationParser)(nil) - -type suffixAnnotationParser struct { - annotationPrefix string -} - -func (p *suffixAnnotationParser) ParseStringAnnotation(annotation string, value *string, annotations map[string]string, opts ...ParseOption) bool { - ret, _ := p.parseStringAnnotation(annotation, value, annotations, opts...) - return ret -} - -func (p *suffixAnnotationParser) ParseBoolAnnotation(annotation string, value *bool, annotations map[string]string, opts ...ParseOption) (bool, error) { - raw := "" - exists, matchedKey := p.parseStringAnnotation(annotation, &raw, annotations, opts...) - if !exists { - return false, nil - } - val, err := strconv.ParseBool(raw) - if err != nil { - return true, errors.Wrapf(err, "failed to parse bool annotation, %v: %v", matchedKey, raw) - } - *value = val - return true, nil -} - -func (p *suffixAnnotationParser) ParseInt64Annotation(annotation string, value *int64, annotations map[string]string, opts ...ParseOption) (bool, error) { - raw := "" - exists, matchedKey := p.parseStringAnnotation(annotation, &raw, annotations, opts...) - if !exists { - return false, nil - } - i, err := strconv.ParseInt(raw, 10, 64) - if err != nil { - return true, errors.Wrapf(err, "failed to parse int64 annotation, %v: %v", matchedKey, raw) - } - *value = i - return true, nil -} - -func (p *suffixAnnotationParser) ParseStringSliceAnnotation(annotation string, value *[]string, annotations map[string]string, opts ...ParseOption) bool { - raw := "" - if exists, _ := p.parseStringAnnotation(annotation, &raw, annotations, opts...); !exists { - return false - } - *value = splitCommaSeparatedString(raw) - return true -} - -func (p *suffixAnnotationParser) ParseJSONAnnotation(annotation string, value interface{}, annotations map[string]string, opts ...ParseOption) (bool, error) { - raw := "" - exists, matchedKey := p.parseStringAnnotation(annotation, &raw, annotations, opts...) - if !exists { - return false, nil - } - if err := json.Unmarshal([]byte(raw), value); err != nil { - return true, errors.Wrapf(err, "failed to parse json annotation, %v: %v", matchedKey, raw) - } - return true, nil -} - -func (p *suffixAnnotationParser) ParseStringMapAnnotation(annotation string, value *map[string]string, annotations map[string]string, opts ...ParseOption) (bool, error) { - raw := "" - exists, matchedKey := p.parseStringAnnotation(annotation, &raw, annotations, opts...) - if !exists { - return false, nil - } - rawKVPairs := splitCommaSeparatedString(raw) - keyValues := make(map[string]string) - for _, kvPair := range rawKVPairs { - parts := strings.SplitN(kvPair, "=", 2) - if len(parts) != 2 { - return false, errors.Errorf("failed to parse stringMap annotation, %v: %v", matchedKey, raw) - } - key := parts[0] - value := parts[1] - if len(key) == 0 { - return false, errors.Errorf("failed to parse stringMap annotation, %v: %v", matchedKey, raw) - } - keyValues[key] = value - } - if value != nil { - *value = keyValues - } - return true, nil -} - -func (p *suffixAnnotationParser) parseStringAnnotation(annotation string, value *string, annotations map[string]string, opts ...ParseOption) (bool, string) { - keys := p.buildAnnotationKeys(annotation, opts...) - for _, key := range keys { - if raw, ok := annotations[key]; ok { - *value = raw - return true, key - } - } - return false, "" -} - -// buildAnnotationKey returns list of full annotation keys based on suffix and parse options -func (p *suffixAnnotationParser) buildAnnotationKeys(suffix string, opts ...ParseOption) []string { - keys := []string{} - parseOpts := ParseOptions{} - for _, opt := range opts { - opt(&parseOpts) - } - if parseOpts.exact { - keys = append(keys, suffix) - } else { - keys = append(keys, fmt.Sprintf("%v/%v", p.annotationPrefix, suffix)) - for _, pfx := range parseOpts.alternativePrefixes { - keys = append(keys, fmt.Sprintf("%v/%v", pfx, suffix)) - } - } - return keys -} - -func splitCommaSeparatedString(commaSeparatedString string) []string { - var result []string - parts := strings.Split(commaSeparatedString, ",") - for _, part := range parts { - part = strings.TrimSpace(part) - if len(part) == 0 { - continue - } - result = append(result, part) - } - return result -} - -var ( - // AnnotationsPrefix is the mutable attribute that the controller explicitly refers to - AnnotationsPrefix = DefaultAnnotationsPrefix -) - -// IngressAnnotation has a method to parse annotations located in Ingress -type IngressAnnotation interface { - Parse(ing *networking.Ingress) (interface{}, error) -} - -type ingAnnotations map[string]string - -func (a ingAnnotations) parseBool(name string) (bool, error) { - val, ok := a[name] - if ok { - b, err := strconv.ParseBool(val) - if err != nil { - return false, NewInvalidAnnotationContent(name, val) - } - return b, nil - } - return false, ErrMissingAnnotations -} - -func (a ingAnnotations) parseString(name string) (string, error) { - val, ok := a[name] - if ok { - s := normalizeString(val) - if len(s) == 0 { - return "", NewInvalidAnnotationContent(name, val) - } - - return s, nil - } - return "", ErrMissingAnnotations -} - -func (a ingAnnotations) parseInt(name string) (int, error) { - val, ok := a[name] - if ok { - i, err := strconv.Atoi(val) - if err != nil { - return 0, NewInvalidAnnotationContent(name, val) - } - return i, nil - } - return 0, ErrMissingAnnotations -} - -func checkAnnotation(name string, ing *networking.Ingress) error { - if ing == nil || len(ing.GetAnnotations()) == 0 { - return ErrMissingAnnotations - } - if name == "" { - return ErrInvalidAnnotationName - } - - return nil -} - -// GetBoolAnnotation extracts a boolean from an Ingress annotation -func GetBoolAnnotation(name string, ing *networking.Ingress) (bool, error) { - v := GetAnnotationWith(name) - err := checkAnnotation(v, ing) - if err != nil { - return false, err - } - return ingAnnotations(ing.GetAnnotations()).parseBool(v) -} - -// GetStringAnnotation extracts a string from an Ingress annotation -func GetStringAnnotation(name string, ing *networking.Ingress) (string, error) { - v := GetAnnotationWith(name) - err := checkAnnotation(v, ing) - if err != nil { - return "", err - } - - return ingAnnotations(ing.GetAnnotations()).parseString(v) -} - -// GetStringAnnotationMutil extracts a string from an Ingress annotation -func GetStringAnnotationMutil(name, name1 string, ing *networking.Ingress) string { - if val, ok := ing.Annotations[name]; ok { - return val - } - if val, ok := ing.Annotations[name1]; ok { - return val - } - return "" -} - -// GetIntAnnotation extracts an int from an Ingress annotation -func GetIntAnnotation(name string, ing *networking.Ingress) (int, error) { - v := GetAnnotationWith(name) - err := checkAnnotation(v, ing) - if err != nil { - return 0, err - } - return ingAnnotations(ing.GetAnnotations()).parseInt(v) -} - -// GetAnnotationWith returns the ingress annotations -func GetAnnotationWith(ann string) string { - return fmt.Sprintf("%v", ann) -} - -func normalizeString(input string) string { - trimmedContent := []string{} - for _, line := range strings.Split(input, "\n") { - trimmedContent = append(trimmedContent, strings.TrimSpace(line)) - } - - return strings.Join(trimmedContent, "\n") -} - -var configmapAnnotations = sets.NewString( - "auth-proxy-set-header", - "fastcgi-params-configmap", -) - -// AnnotationsReferencesConfigmap checks if at least one annotation in the Ingress rule -// references a configmap. -func AnnotationsReferencesConfigmap(ing *networking.Ingress) bool { - if ing == nil || len(ing.GetAnnotations()) == 0 { - return false - } - - for name := range ing.GetAnnotations() { - if configmapAnnotations.Has(name) { - return true - } - } - - return false -} - -// StringToURL parses the provided string into URL and returns error -// message in case of failure -func StringToURL(input string) (*url.URL, error) { - parsedURL, err := url.Parse(input) - if err != nil { - return nil, fmt.Errorf("%v is not a valid URL: %v", input, err) - } - - if parsedURL.Scheme == "" { - return nil, fmt.Errorf("url scheme is empty") - } else if parsedURL.Host == "" { - return nil, fmt.Errorf("url host is empty") - } else if strings.Contains(parsedURL.Host, "..") { - return nil, fmt.Errorf("invalid url host") - } - - return parsedURL, nil -} - -var ( - // ErrMissingAnnotations the ingress rule does not contain annotations - // This is an error only when annotations are being parsed - ErrMissingAnnotations = errors.New("ingress rule without annotations") - - // ErrInvalidAnnotationName the ingress rule does contains an invalid - // annotation name - ErrInvalidAnnotationName = errors.New("invalid annotation name") -) - -// NewInvalidAnnotationConfiguration returns a new InvalidConfiguration error for use when -// annotations are not correctly configured -func NewInvalidAnnotationConfiguration(name string, reason string) error { - return InvalidConfiguration{ - Name: fmt.Sprintf("the annotation %v does not contain a valid configuration: %v", name, reason), - } -} - -// NewInvalidAnnotationContent returns a new InvalidContent error -func NewInvalidAnnotationContent(name string, val interface{}) error { - return InvalidContent{ - Name: fmt.Sprintf("the annotation %v does not contain a valid value (%v)", name, val), - } -} - -// NewLocationDenied returns a new LocationDenied error -func NewLocationDenied(reason string) error { - return LocationDenied{ - Reason: errors.Errorf("Location denied, reason: %v", reason), - } -} - -// InvalidConfiguration Error -type InvalidConfiguration struct { - Name string -} - -func (e InvalidConfiguration) Error() string { - return e.Name -} - -// InvalidContent error -type InvalidContent struct { - Name string -} - -func (e InvalidContent) Error() string { - return e.Name -} - -// LocationDenied error -type LocationDenied struct { - Reason error -} - -func (e LocationDenied) Error() string { - return e.Reason.Error() -} - -// IsLocationDenied checks if the err is an error which -// indicates a location should return HTTP code 503 -func IsLocationDenied(e error) bool { - _, ok := e.(LocationDenied) - return ok -} - -// IsMissingAnnotations checks if the err is an error which -// indicates the ingress does not contain annotations -func IsMissingAnnotations(e error) bool { - return e == ErrMissingAnnotations -} - -// IsInvalidContent checks if the err is an error which -// indicates an annotations value is not valid -func IsInvalidContent(e error) bool { - _, ok := e.(InvalidContent) - return ok -} - -// New returns a new error -func New(m string) error { - return errors.New(m) -} - -// Errorf formats according to a format specifier and returns the string -// as a value that satisfies error. -func Errorf(format string, args ...interface{}) error { - return errors.Errorf(format, args...) -}