diff --git a/apis/concierge/config/v1alpha1/types_credentialissuer.go.tmpl b/apis/concierge/config/v1alpha1/types_credentialissuer.go.tmpl index 3c0c8ba0..60edeccc 100644 --- a/apis/concierge/config/v1alpha1/types_credentialissuer.go.tmpl +++ b/apis/concierge/config/v1alpha1/types_credentialissuer.go.tmpl @@ -3,17 +3,23 @@ package v1alpha1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) +// StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. // +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy type StrategyType string +// FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. // +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy type FrontendType string +// StrategyStatus enumerates whether a strategy is working on a cluster. // +kubebuilder:validation:Enum=Success;Error type StrategyStatus string +// StrategyReason enumerates the detailed reason why a strategy is in a particular status. // +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey type StrategyReason string @@ -36,7 +42,91 @@ const ( FetchedKeyStrategyReason = StrategyReason("FetchedKey") ) -// Status of a credential issuer. +// CredentialIssuerSpec describes the intended configuration of the Concierge. +type CredentialIssuerSpec struct { + // ImpersonationProxy describes the intended configuration of the Concierge impersonation proxy. + ImpersonationProxy *ImpersonationProxySpec `json:"impersonationProxy"` +} + +// ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// +// +kubebuilder:validation:Enum=auto;enabled;disabled +type ImpersonationProxyMode string + +const ( + // ImpersonationProxyModeDisabled explicitly disables the impersonation proxy. + ImpersonationProxyModeDisabled = ImpersonationProxyMode("disabled") + + // ImpersonationProxyModeEnabled explicitly enables the impersonation proxy. + ImpersonationProxyModeEnabled = ImpersonationProxyMode("enabled") + + // ImpersonationProxyModeAuto enables or disables the impersonation proxy based upon the cluster in which it is running. + ImpersonationProxyModeAuto = ImpersonationProxyMode("auto") +) + +// ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// +// +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None +type ImpersonationProxyServiceType string + +const ( + // ImpersonationProxyServiceTypeLoadBalancer provisions a service of type LoadBalancer. + ImpersonationProxyServiceTypeLoadBalancer = ImpersonationProxyServiceType("LoadBalancer") + + // ImpersonationProxyServiceTypeClusterIP provisions a service of type ClusterIP. + ImpersonationProxyServiceTypeClusterIP = ImpersonationProxyServiceType("ClusterIP") + + // ImpersonationProxyServiceTypeNone does not automatically provision any service. + ImpersonationProxyServiceTypeNone = ImpersonationProxyServiceType("None") +) + +// ImpersonationProxySpec describes the intended configuration of the Concierge impersonation proxy. +type ImpersonationProxySpec struct { + // Mode configures whether the impersonation proxy should be started: + // - "disabled" explicitly disables the impersonation proxy. This is the default. + // - "enabled" explicitly enables the impersonation proxy. + // - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running. + Mode ImpersonationProxyMode `json:"mode"` + + // Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients. + // + // +kubebuilder:default:={"type": "LoadBalancer"} + Service ImpersonationProxyServiceSpec `json:"service"` + + // ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will + // be served using the external name of the LoadBalancer service or the cluster service DNS name. + // + // This field must be non-empty when spec.impersonationProxy.service.mode is "None". + // + // +optional + ExternalEndpoint string `json:"externalEndpoint,omitempty"` +} + +// ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy. +type ImpersonationProxyServiceSpec struct { + // Type specifies the type of Service to provision for the impersonation proxy. + // + // If the type is "None", then the "spec.impersonationProxy.externalEndpoint" field must be set to a non-empty + // value so that the Concierge can properly advertise the endpoint in the CredentialIssuer's status. + // + // +kubebuilder:default:="LoadBalancer" + Type ImpersonationProxyServiceType `json:"type,omitempty"` + + // LoadBalancerIP specifies the IP address to set in the spec.loadBalancerIP field of the provisioned Service. + // This is not supported on all cloud providers. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=255 + // +optional + LoadBalancerIP string `json:"loadBalancerIP,omitempty"` + + // Annotations specifies zero or more key/value pairs to set as annotations on the provisioned Service. + // + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + +// CredentialIssuerStatus describes the status of the Concierge. type CredentialIssuerStatus struct { // List of integration strategies that were attempted by Pinniped. Strategies []CredentialIssuerStrategy `json:"strategies"` @@ -47,7 +137,8 @@ type CredentialIssuerStatus struct { KubeConfigInfo *CredentialIssuerKubeConfigInfo `json:"kubeConfigInfo,omitempty"` } -// Information needed to form a valid Pinniped-based kubeconfig using this credential issuer. +// CredentialIssuerKubeConfigInfo provides the information needed to form a valid Pinniped-based kubeconfig using this credential issuer. +// This type is deprecated and will be removed in a future version. type CredentialIssuerKubeConfigInfo struct { // The K8s API server URL. // +kubebuilder:validation:MinLength=1 @@ -59,7 +150,7 @@ type CredentialIssuerKubeConfigInfo struct { CertificateAuthorityData string `json:"certificateAuthorityData"` } -// Status of an integration strategy that was attempted by Pinniped. +// CredentialIssuerStrategy describes the status of an integration strategy that was attempted by Pinniped. type CredentialIssuerStrategy struct { // Type of integration attempted. Type StrategyType `json:"type"` @@ -81,6 +172,7 @@ type CredentialIssuerStrategy struct { Frontend *CredentialIssuerFrontend `json:"frontend,omitempty"` } +// CredentialIssuerFrontend describes how to connect using a particular integration strategy. type CredentialIssuerFrontend struct { // Type describes which frontend mechanism clients can use with a strategy. Type FrontendType `json:"type"` @@ -118,7 +210,7 @@ type ImpersonationProxyInfo struct { CertificateAuthorityData string `json:"certificateAuthorityData"` } -// Describes the configuration status of a Pinniped credential issuer. +// CredentialIssuer describes the configuration and status of the Pinniped Concierge credential issuer. // +genclient // +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -128,12 +220,18 @@ type CredentialIssuer struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - // Status of the credential issuer. + // Spec describes the intended configuration of the Concierge. + // + // +optional + Spec CredentialIssuerSpec `json:"spec"` + + // CredentialIssuerStatus describes the status of the Concierge. + // // +optional Status CredentialIssuerStatus `json:"status"` } -// List of CredentialIssuer objects. +// CredentialIssuerList is a list of CredentialIssuer objects. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type CredentialIssuerList struct { metav1.TypeMeta `json:",inline"` diff --git a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl index 0e28234d..74570a92 100644 --- a/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl +++ b/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go.tmpl @@ -68,8 +68,8 @@ type LDAPIdentityProviderGroupSearchAttributes struct { // GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP - // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, the default will act as if the GroupName were specified as "cn" (common name). + // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". + // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). // +optional GroupName string `json:"groupName,omitempty"` } diff --git a/deploy/concierge/config.concierge.pinniped.dev_credentialissuers.yaml b/deploy/concierge/config.concierge.pinniped.dev_credentialissuers.yaml index 8123f238..7b94b098 100644 --- a/deploy/concierge/config.concierge.pinniped.dev_credentialissuers.yaml +++ b/deploy/concierge/config.concierge.pinniped.dev_credentialissuers.yaml @@ -21,7 +21,8 @@ spec: - name: v1alpha1 schema: openAPIV3Schema: - description: Describes the configuration status of a Pinniped credential issuer. + description: CredentialIssuer describes the configuration and status of the + Pinniped Concierge credential issuer. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -35,8 +36,73 @@ spec: type: string metadata: type: object + spec: + description: Spec describes the intended configuration of the Concierge. + properties: + impersonationProxy: + description: ImpersonationProxy describes the intended configuration + of the Concierge impersonation proxy. + properties: + externalEndpoint: + description: "ExternalEndpoint describes the HTTPS endpoint where + the proxy will be exposed. If not set, the proxy will be served + using the external name of the LoadBalancer service or the cluster + service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.mode + is \"None\"." + type: string + mode: + description: 'Mode configures whether the impersonation proxy + should be started: - "disabled" explicitly disables the impersonation + proxy. This is the default. - "enabled" explicitly enables the + impersonation proxy. - "auto" enables or disables the impersonation + proxy based upon the cluster in which it is running.' + enum: + - auto + - enabled + - disabled + type: string + service: + default: + type: LoadBalancer + description: Service describes the configuration of the Service + provisioned to expose the impersonation proxy to clients. + properties: + annotations: + additionalProperties: + type: string + description: Annotations specifies zero or more key/value + pairs to set as annotations on the provisioned Service. + type: object + loadBalancerIP: + description: LoadBalancerIP specifies the IP address to set + in the spec.loadBalancerIP field of the provisioned Service. + This is not supported on all cloud providers. + maxLength: 255 + minLength: 1 + type: string + type: + default: LoadBalancer + description: "Type specifies the type of Service to provision + for the impersonation proxy. \n If the type is \"None\", + then the \"spec.impersonationProxy.externalEndpoint\" field + must be set to a non-empty value so that the Concierge can + properly advertise the endpoint in the CredentialIssuer's + status." + enum: + - LoadBalancer + - ClusterIP + - None + type: string + type: object + required: + - mode + - service + type: object + required: + - impersonationProxy + type: object status: - description: Status of the credential issuer. + description: CredentialIssuerStatus describes the status of the Concierge. properties: kubeConfigInfo: description: Information needed to form a valid Pinniped-based kubeconfig @@ -60,8 +126,8 @@ spec: description: List of integration strategies that were attempted by Pinniped. items: - description: Status of an integration strategy that was attempted - by Pinniped. + description: CredentialIssuerStrategy describes the status of an + integration strategy that was attempted by Pinniped. properties: frontend: description: Frontend describes how clients can connect using diff --git a/deploy/concierge/deployment.yaml b/deploy/concierge/deployment.yaml index 8add6edd..31c7fe68 100644 --- a/deploy/concierge/deployment.yaml +++ b/deploy/concierge/deployment.yaml @@ -49,8 +49,8 @@ data: servingCertificateSecret: (@= defaultResourceNameWithSuffix("api-tls-serving-certificate") @) credentialIssuer: (@= defaultResourceNameWithSuffix("config") @) apiService: (@= defaultResourceNameWithSuffix("api") @) - impersonationConfigMap: (@= defaultResourceNameWithSuffix("impersonation-proxy-config") @) impersonationLoadBalancerService: (@= defaultResourceNameWithSuffix("impersonation-proxy-load-balancer") @) + impersonationClusterIPService: (@= defaultResourceNameWithSuffix("impersonation-proxy-cluster-ip") @) impersonationTLSCertificateSecret: (@= defaultResourceNameWithSuffix("impersonation-proxy-tls-serving-certificate") @) impersonationCACertificateSecret: (@= defaultResourceNameWithSuffix("impersonation-proxy-ca-certificate") @) impersonationSignerSecret: (@= defaultResourceNameWithSuffix("impersonation-proxy-signer-ca-certificate") @) @@ -253,3 +253,15 @@ kind: CredentialIssuer metadata: name: #@ defaultResourceNameWithSuffix("config") labels: #@ labels() +spec: + impersonationProxy: + mode: #@ data.values.impersonation_proxy_spec.mode + #@ if data.values.impersonation_proxy_spec.external_endpoint: + externalEndpoint: #@ data.values.impersonation_proxy_spec.external_endpoint + #@ end + service: + mode: #@ data.values.impersonation_proxy_spec.service.mode + #@ if data.values.impersonation_proxy_spec.service.load_balancer_ip: + loadBalancerIP: #@ data.values.impersonation_proxy_spec.service.load_balancer_ip + #@ end + annotations: #@ data.values.impersonation_proxy_spec.service.annotations diff --git a/deploy/concierge/values.yaml b/deploy/concierge/values.yaml index 05d550db..0445afb2 100644 --- a/deploy/concierge/values.yaml +++ b/deploy/concierge/values.yaml @@ -63,3 +63,33 @@ run_as_group: 1001 #! run_as_group specifies the group ID that will own the proc #! authentication.concierge.pinniped.dev, etc. As an example, if this is set to tuna.io, then #! Pinniped API groups will look like foo.tuna.io. authentication.concierge.tuna.io, etc. api_group_suffix: pinniped.dev + +#! Customize CredentialIssuer.spec.impersonationProxy to change how the concierge +#! handles impersonation. +impersonation_proxy_spec: + #! options are "auto", "disabled" or "enabled". + #! If auto, the impersonation proxy will run only if the cluster signing key is not available + #! and the other strategy does not work. + #! If disabled, the impersonation proxy will never run, which could mean that the concierge + #! doesn't work at all. + #! If enabled, the impersonation proxy will always run regardless of other strategies available. + mode: auto + #! The endpoint which the client should use to connect to the impersonation proxy. + #! If left unset, the client will default to connecting based on the ClusterIP or LoadBalancer + #! endpoint. + external_endpoint: + service: + #! Options are "LoadBalancer", "ClusterIP" and "None". + #! LoadBalancer automatically provisions a Service of type LoadBalancer pointing at + #! the impersonation proxy. Some cloud providers will allocate + #! a public IP address by default even on private clusters. + #! ClusterIP automatically provisions a Service of type ClusterIP pointing at the + #! impersonation proxy. + #! None does not provision either and assumes that you have set the external_endpoint + #! and set up your own ingress to connect to the impersonation proxy. + mode: LoadBalancer + #! The annotations that should be set on the ClusterIP or LoadBalancer Service. + annotations: + {service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "4000"} + #! When mode LoadBalancer is set, this will set the LoadBalancer Service's Spec.LoadBalancerIP. + load_balancer_ip: diff --git a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 46fbe1d0..ed9780b5 100644 --- a/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/deploy/supervisor/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -86,10 +86,10 @@ spec: in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server - in the user's entry. Distinguished names can be used by - specifying lower-case "dn". Optional. When not specified, - the default will act as if the GroupName were specified - as "cn" (common name). + in the user's entry. E.g. "cn" for common name. Distinguished + names can be used by specifying lower-case "dn". Optional. + When not specified, the default will act as if the GroupName + were specified as "dn" (distinguished name). type: string type: object base: diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index b08040ec..e9637cbb 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -220,7 +220,7 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge configuration [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuer"] ==== CredentialIssuer -Describes the configuration status of a Pinniped credential issuer. +CredentialIssuer describes the configuration and status of the Pinniped Concierge credential issuer. .Appears In: **** @@ -232,7 +232,8 @@ Describes the configuration status of a Pinniped credential issuer. | Field | Description | *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. -| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerstatus[$$CredentialIssuerStatus$$]__ | Status of the credential issuer. +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerspec[$$CredentialIssuerSpec$$]__ | Spec describes the intended configuration of the Concierge. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerstatus[$$CredentialIssuerStatus$$]__ | CredentialIssuerStatus describes the status of the Concierge. |=== @@ -275,10 +276,27 @@ Describes the configuration status of a Pinniped credential issuer. +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerspec"] +==== CredentialIssuerSpec + +CredentialIssuerSpec describes the intended configuration of the Concierge. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuer[$$CredentialIssuer$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`impersonationProxy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$]__ | ImpersonationProxy describes the intended configuration of the Concierge impersonation proxy. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerstatus"] ==== CredentialIssuerStatus -Status of a credential issuer. +CredentialIssuerStatus describes the status of the Concierge. .Appears In: **** @@ -333,6 +351,70 @@ Status of a credential issuer. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-impersonationproxymode"] +==== ImpersonationProxyMode (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-impersonationproxyservicespec"] +==== ImpersonationProxyServiceSpec + +ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __ImpersonationProxyServiceType__ | Type specifies the type of Service to provision for the impersonation proxy. + If the type is "None", then the "spec.impersonationProxy.externalEndpoint" field must be set to a non-empty value so that the Concierge can properly advertise the endpoint in the CredentialIssuer's status. +| *`loadBalancerIP`* __string__ | LoadBalancerIP specifies the IP address to set in the spec.loadBalancerIP field of the provisioned Service. This is not supported on all cloud providers. +| *`annotations`* __object (keys:string, values:string)__ | Annotations specifies zero or more key/value pairs to set as annotations on the provisioned Service. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-impersonationproxyservicetype"] +==== ImpersonationProxyServiceType (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-impersonationproxyservicespec[$$ImpersonationProxyServiceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-impersonationproxyspec"] +==== ImpersonationProxySpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-credentialissuerspec[$$CredentialIssuerSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`mode`* __ImpersonationProxyMode__ | Mode configures whether the impersonation proxy should be started: - "disabled" explicitly disables the impersonation proxy. This is the default. - "enabled" explicitly enables the impersonation proxy. - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running. +| *`service`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-impersonationproxyservicespec[$$ImpersonationProxyServiceSpec$$]__ | Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients. +| *`externalEndpoint`* __string__ | ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will be served using the external name of the LoadBalancer service or the cluster service DNS name. + This field must be non-empty when spec.impersonationProxy.service.mode is "None". +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo"] ==== TokenCredentialRequestAPIInfo @@ -770,7 +852,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "cn" (common name). +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). |=== diff --git a/generated/1.17/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/1.17/apis/concierge/config/v1alpha1/types_credentialissuer.go index 3c0c8ba0..60edeccc 100644 --- a/generated/1.17/apis/concierge/config/v1alpha1/types_credentialissuer.go +++ b/generated/1.17/apis/concierge/config/v1alpha1/types_credentialissuer.go @@ -3,17 +3,23 @@ package v1alpha1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) +// StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. // +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy type StrategyType string +// FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. // +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy type FrontendType string +// StrategyStatus enumerates whether a strategy is working on a cluster. // +kubebuilder:validation:Enum=Success;Error type StrategyStatus string +// StrategyReason enumerates the detailed reason why a strategy is in a particular status. // +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey type StrategyReason string @@ -36,7 +42,91 @@ const ( FetchedKeyStrategyReason = StrategyReason("FetchedKey") ) -// Status of a credential issuer. +// CredentialIssuerSpec describes the intended configuration of the Concierge. +type CredentialIssuerSpec struct { + // ImpersonationProxy describes the intended configuration of the Concierge impersonation proxy. + ImpersonationProxy *ImpersonationProxySpec `json:"impersonationProxy"` +} + +// ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// +// +kubebuilder:validation:Enum=auto;enabled;disabled +type ImpersonationProxyMode string + +const ( + // ImpersonationProxyModeDisabled explicitly disables the impersonation proxy. + ImpersonationProxyModeDisabled = ImpersonationProxyMode("disabled") + + // ImpersonationProxyModeEnabled explicitly enables the impersonation proxy. + ImpersonationProxyModeEnabled = ImpersonationProxyMode("enabled") + + // ImpersonationProxyModeAuto enables or disables the impersonation proxy based upon the cluster in which it is running. + ImpersonationProxyModeAuto = ImpersonationProxyMode("auto") +) + +// ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// +// +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None +type ImpersonationProxyServiceType string + +const ( + // ImpersonationProxyServiceTypeLoadBalancer provisions a service of type LoadBalancer. + ImpersonationProxyServiceTypeLoadBalancer = ImpersonationProxyServiceType("LoadBalancer") + + // ImpersonationProxyServiceTypeClusterIP provisions a service of type ClusterIP. + ImpersonationProxyServiceTypeClusterIP = ImpersonationProxyServiceType("ClusterIP") + + // ImpersonationProxyServiceTypeNone does not automatically provision any service. + ImpersonationProxyServiceTypeNone = ImpersonationProxyServiceType("None") +) + +// ImpersonationProxySpec describes the intended configuration of the Concierge impersonation proxy. +type ImpersonationProxySpec struct { + // Mode configures whether the impersonation proxy should be started: + // - "disabled" explicitly disables the impersonation proxy. This is the default. + // - "enabled" explicitly enables the impersonation proxy. + // - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running. + Mode ImpersonationProxyMode `json:"mode"` + + // Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients. + // + // +kubebuilder:default:={"type": "LoadBalancer"} + Service ImpersonationProxyServiceSpec `json:"service"` + + // ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will + // be served using the external name of the LoadBalancer service or the cluster service DNS name. + // + // This field must be non-empty when spec.impersonationProxy.service.mode is "None". + // + // +optional + ExternalEndpoint string `json:"externalEndpoint,omitempty"` +} + +// ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy. +type ImpersonationProxyServiceSpec struct { + // Type specifies the type of Service to provision for the impersonation proxy. + // + // If the type is "None", then the "spec.impersonationProxy.externalEndpoint" field must be set to a non-empty + // value so that the Concierge can properly advertise the endpoint in the CredentialIssuer's status. + // + // +kubebuilder:default:="LoadBalancer" + Type ImpersonationProxyServiceType `json:"type,omitempty"` + + // LoadBalancerIP specifies the IP address to set in the spec.loadBalancerIP field of the provisioned Service. + // This is not supported on all cloud providers. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=255 + // +optional + LoadBalancerIP string `json:"loadBalancerIP,omitempty"` + + // Annotations specifies zero or more key/value pairs to set as annotations on the provisioned Service. + // + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + +// CredentialIssuerStatus describes the status of the Concierge. type CredentialIssuerStatus struct { // List of integration strategies that were attempted by Pinniped. Strategies []CredentialIssuerStrategy `json:"strategies"` @@ -47,7 +137,8 @@ type CredentialIssuerStatus struct { KubeConfigInfo *CredentialIssuerKubeConfigInfo `json:"kubeConfigInfo,omitempty"` } -// Information needed to form a valid Pinniped-based kubeconfig using this credential issuer. +// CredentialIssuerKubeConfigInfo provides the information needed to form a valid Pinniped-based kubeconfig using this credential issuer. +// This type is deprecated and will be removed in a future version. type CredentialIssuerKubeConfigInfo struct { // The K8s API server URL. // +kubebuilder:validation:MinLength=1 @@ -59,7 +150,7 @@ type CredentialIssuerKubeConfigInfo struct { CertificateAuthorityData string `json:"certificateAuthorityData"` } -// Status of an integration strategy that was attempted by Pinniped. +// CredentialIssuerStrategy describes the status of an integration strategy that was attempted by Pinniped. type CredentialIssuerStrategy struct { // Type of integration attempted. Type StrategyType `json:"type"` @@ -81,6 +172,7 @@ type CredentialIssuerStrategy struct { Frontend *CredentialIssuerFrontend `json:"frontend,omitempty"` } +// CredentialIssuerFrontend describes how to connect using a particular integration strategy. type CredentialIssuerFrontend struct { // Type describes which frontend mechanism clients can use with a strategy. Type FrontendType `json:"type"` @@ -118,7 +210,7 @@ type ImpersonationProxyInfo struct { CertificateAuthorityData string `json:"certificateAuthorityData"` } -// Describes the configuration status of a Pinniped credential issuer. +// CredentialIssuer describes the configuration and status of the Pinniped Concierge credential issuer. // +genclient // +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -128,12 +220,18 @@ type CredentialIssuer struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - // Status of the credential issuer. + // Spec describes the intended configuration of the Concierge. + // + // +optional + Spec CredentialIssuerSpec `json:"spec"` + + // CredentialIssuerStatus describes the status of the Concierge. + // // +optional Status CredentialIssuerStatus `json:"status"` } -// List of CredentialIssuer objects. +// CredentialIssuerList is a list of CredentialIssuer objects. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type CredentialIssuerList struct { metav1.TypeMeta `json:",inline"` diff --git a/generated/1.17/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.17/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go index 4a3ad605..cf679b2c 100644 --- a/generated/1.17/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.17/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go @@ -16,6 +16,7 @@ func (in *CredentialIssuer) DeepCopyInto(out *CredentialIssuer) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) return } @@ -113,6 +114,27 @@ func (in *CredentialIssuerList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CredentialIssuerSpec) DeepCopyInto(out *CredentialIssuerSpec) { + *out = *in + if in.ImpersonationProxy != nil { + in, out := &in.ImpersonationProxy, &out.ImpersonationProxy + *out = new(ImpersonationProxySpec) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialIssuerSpec. +func (in *CredentialIssuerSpec) DeepCopy() *CredentialIssuerSpec { + if in == nil { + return nil + } + out := new(CredentialIssuerSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CredentialIssuerStatus) DeepCopyInto(out *CredentialIssuerStatus) { *out = *in @@ -179,6 +201,46 @@ func (in *ImpersonationProxyInfo) DeepCopy() *ImpersonationProxyInfo { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImpersonationProxyServiceSpec) DeepCopyInto(out *ImpersonationProxyServiceSpec) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxyServiceSpec. +func (in *ImpersonationProxyServiceSpec) DeepCopy() *ImpersonationProxyServiceSpec { + if in == nil { + return nil + } + out := new(ImpersonationProxyServiceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImpersonationProxySpec) DeepCopyInto(out *ImpersonationProxySpec) { + *out = *in + in.Service.DeepCopyInto(&out.Service) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxySpec. +func (in *ImpersonationProxySpec) DeepCopy() *ImpersonationProxySpec { + if in == nil { + return nil + } + out := new(ImpersonationProxySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) { *out = *in diff --git a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0e28234d..74570a92 100644 --- a/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.17/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -68,8 +68,8 @@ type LDAPIdentityProviderGroupSearchAttributes struct { // GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP - // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, the default will act as if the GroupName were specified as "cn" (common name). + // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". + // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). // +optional GroupName string `json:"groupName,omitempty"` } diff --git a/generated/1.17/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.17/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8123f238..7b94b098 100644 --- a/generated/1.17/crds/config.concierge.pinniped.dev_credentialissuers.yaml +++ b/generated/1.17/crds/config.concierge.pinniped.dev_credentialissuers.yaml @@ -21,7 +21,8 @@ spec: - name: v1alpha1 schema: openAPIV3Schema: - description: Describes the configuration status of a Pinniped credential issuer. + description: CredentialIssuer describes the configuration and status of the + Pinniped Concierge credential issuer. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -35,8 +36,73 @@ spec: type: string metadata: type: object + spec: + description: Spec describes the intended configuration of the Concierge. + properties: + impersonationProxy: + description: ImpersonationProxy describes the intended configuration + of the Concierge impersonation proxy. + properties: + externalEndpoint: + description: "ExternalEndpoint describes the HTTPS endpoint where + the proxy will be exposed. If not set, the proxy will be served + using the external name of the LoadBalancer service or the cluster + service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.mode + is \"None\"." + type: string + mode: + description: 'Mode configures whether the impersonation proxy + should be started: - "disabled" explicitly disables the impersonation + proxy. This is the default. - "enabled" explicitly enables the + impersonation proxy. - "auto" enables or disables the impersonation + proxy based upon the cluster in which it is running.' + enum: + - auto + - enabled + - disabled + type: string + service: + default: + type: LoadBalancer + description: Service describes the configuration of the Service + provisioned to expose the impersonation proxy to clients. + properties: + annotations: + additionalProperties: + type: string + description: Annotations specifies zero or more key/value + pairs to set as annotations on the provisioned Service. + type: object + loadBalancerIP: + description: LoadBalancerIP specifies the IP address to set + in the spec.loadBalancerIP field of the provisioned Service. + This is not supported on all cloud providers. + maxLength: 255 + minLength: 1 + type: string + type: + default: LoadBalancer + description: "Type specifies the type of Service to provision + for the impersonation proxy. \n If the type is \"None\", + then the \"spec.impersonationProxy.externalEndpoint\" field + must be set to a non-empty value so that the Concierge can + properly advertise the endpoint in the CredentialIssuer's + status." + enum: + - LoadBalancer + - ClusterIP + - None + type: string + type: object + required: + - mode + - service + type: object + required: + - impersonationProxy + type: object status: - description: Status of the credential issuer. + description: CredentialIssuerStatus describes the status of the Concierge. properties: kubeConfigInfo: description: Information needed to form a valid Pinniped-based kubeconfig @@ -60,8 +126,8 @@ spec: description: List of integration strategies that were attempted by Pinniped. items: - description: Status of an integration strategy that was attempted - by Pinniped. + description: CredentialIssuerStrategy describes the status of an + integration strategy that was attempted by Pinniped. properties: frontend: description: Frontend describes how clients can connect using diff --git a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 46fbe1d0..ed9780b5 100644 --- a/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.17/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -86,10 +86,10 @@ spec: in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server - in the user's entry. Distinguished names can be used by - specifying lower-case "dn". Optional. When not specified, - the default will act as if the GroupName were specified - as "cn" (common name). + in the user's entry. E.g. "cn" for common name. Distinguished + names can be used by specifying lower-case "dn". Optional. + When not specified, the default will act as if the GroupName + were specified as "dn" (distinguished name). type: string type: object base: diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index d901b8be..3fbd9d46 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -220,7 +220,7 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge configuration [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-credentialissuer"] ==== CredentialIssuer -Describes the configuration status of a Pinniped credential issuer. +CredentialIssuer describes the configuration and status of the Pinniped Concierge credential issuer. .Appears In: **** @@ -232,7 +232,8 @@ Describes the configuration status of a Pinniped credential issuer. | Field | Description | *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. -| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-credentialissuerstatus[$$CredentialIssuerStatus$$]__ | Status of the credential issuer. +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-credentialissuerspec[$$CredentialIssuerSpec$$]__ | Spec describes the intended configuration of the Concierge. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-credentialissuerstatus[$$CredentialIssuerStatus$$]__ | CredentialIssuerStatus describes the status of the Concierge. |=== @@ -275,10 +276,27 @@ Describes the configuration status of a Pinniped credential issuer. +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-credentialissuerspec"] +==== CredentialIssuerSpec + +CredentialIssuerSpec describes the intended configuration of the Concierge. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-credentialissuer[$$CredentialIssuer$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`impersonationProxy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$]__ | ImpersonationProxy describes the intended configuration of the Concierge impersonation proxy. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-credentialissuerstatus"] ==== CredentialIssuerStatus -Status of a credential issuer. +CredentialIssuerStatus describes the status of the Concierge. .Appears In: **** @@ -333,6 +351,70 @@ Status of a credential issuer. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-impersonationproxymode"] +==== ImpersonationProxyMode (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-impersonationproxyservicespec"] +==== ImpersonationProxyServiceSpec + +ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __ImpersonationProxyServiceType__ | Type specifies the type of Service to provision for the impersonation proxy. + If the type is "None", then the "spec.impersonationProxy.externalEndpoint" field must be set to a non-empty value so that the Concierge can properly advertise the endpoint in the CredentialIssuer's status. +| *`loadBalancerIP`* __string__ | LoadBalancerIP specifies the IP address to set in the spec.loadBalancerIP field of the provisioned Service. This is not supported on all cloud providers. +| *`annotations`* __object (keys:string, values:string)__ | Annotations specifies zero or more key/value pairs to set as annotations on the provisioned Service. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-impersonationproxyservicetype"] +==== ImpersonationProxyServiceType (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-impersonationproxyservicespec[$$ImpersonationProxyServiceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-impersonationproxyspec"] +==== ImpersonationProxySpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-credentialissuerspec[$$CredentialIssuerSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`mode`* __ImpersonationProxyMode__ | Mode configures whether the impersonation proxy should be started: - "disabled" explicitly disables the impersonation proxy. This is the default. - "enabled" explicitly enables the impersonation proxy. - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running. +| *`service`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-impersonationproxyservicespec[$$ImpersonationProxyServiceSpec$$]__ | Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients. +| *`externalEndpoint`* __string__ | ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will be served using the external name of the LoadBalancer service or the cluster service DNS name. + This field must be non-empty when spec.impersonationProxy.service.mode is "None". +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo"] ==== TokenCredentialRequestAPIInfo @@ -770,7 +852,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "cn" (common name). +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). |=== diff --git a/generated/1.18/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/1.18/apis/concierge/config/v1alpha1/types_credentialissuer.go index 3c0c8ba0..60edeccc 100644 --- a/generated/1.18/apis/concierge/config/v1alpha1/types_credentialissuer.go +++ b/generated/1.18/apis/concierge/config/v1alpha1/types_credentialissuer.go @@ -3,17 +3,23 @@ package v1alpha1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) +// StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. // +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy type StrategyType string +// FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. // +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy type FrontendType string +// StrategyStatus enumerates whether a strategy is working on a cluster. // +kubebuilder:validation:Enum=Success;Error type StrategyStatus string +// StrategyReason enumerates the detailed reason why a strategy is in a particular status. // +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey type StrategyReason string @@ -36,7 +42,91 @@ const ( FetchedKeyStrategyReason = StrategyReason("FetchedKey") ) -// Status of a credential issuer. +// CredentialIssuerSpec describes the intended configuration of the Concierge. +type CredentialIssuerSpec struct { + // ImpersonationProxy describes the intended configuration of the Concierge impersonation proxy. + ImpersonationProxy *ImpersonationProxySpec `json:"impersonationProxy"` +} + +// ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// +// +kubebuilder:validation:Enum=auto;enabled;disabled +type ImpersonationProxyMode string + +const ( + // ImpersonationProxyModeDisabled explicitly disables the impersonation proxy. + ImpersonationProxyModeDisabled = ImpersonationProxyMode("disabled") + + // ImpersonationProxyModeEnabled explicitly enables the impersonation proxy. + ImpersonationProxyModeEnabled = ImpersonationProxyMode("enabled") + + // ImpersonationProxyModeAuto enables or disables the impersonation proxy based upon the cluster in which it is running. + ImpersonationProxyModeAuto = ImpersonationProxyMode("auto") +) + +// ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// +// +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None +type ImpersonationProxyServiceType string + +const ( + // ImpersonationProxyServiceTypeLoadBalancer provisions a service of type LoadBalancer. + ImpersonationProxyServiceTypeLoadBalancer = ImpersonationProxyServiceType("LoadBalancer") + + // ImpersonationProxyServiceTypeClusterIP provisions a service of type ClusterIP. + ImpersonationProxyServiceTypeClusterIP = ImpersonationProxyServiceType("ClusterIP") + + // ImpersonationProxyServiceTypeNone does not automatically provision any service. + ImpersonationProxyServiceTypeNone = ImpersonationProxyServiceType("None") +) + +// ImpersonationProxySpec describes the intended configuration of the Concierge impersonation proxy. +type ImpersonationProxySpec struct { + // Mode configures whether the impersonation proxy should be started: + // - "disabled" explicitly disables the impersonation proxy. This is the default. + // - "enabled" explicitly enables the impersonation proxy. + // - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running. + Mode ImpersonationProxyMode `json:"mode"` + + // Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients. + // + // +kubebuilder:default:={"type": "LoadBalancer"} + Service ImpersonationProxyServiceSpec `json:"service"` + + // ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will + // be served using the external name of the LoadBalancer service or the cluster service DNS name. + // + // This field must be non-empty when spec.impersonationProxy.service.mode is "None". + // + // +optional + ExternalEndpoint string `json:"externalEndpoint,omitempty"` +} + +// ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy. +type ImpersonationProxyServiceSpec struct { + // Type specifies the type of Service to provision for the impersonation proxy. + // + // If the type is "None", then the "spec.impersonationProxy.externalEndpoint" field must be set to a non-empty + // value so that the Concierge can properly advertise the endpoint in the CredentialIssuer's status. + // + // +kubebuilder:default:="LoadBalancer" + Type ImpersonationProxyServiceType `json:"type,omitempty"` + + // LoadBalancerIP specifies the IP address to set in the spec.loadBalancerIP field of the provisioned Service. + // This is not supported on all cloud providers. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=255 + // +optional + LoadBalancerIP string `json:"loadBalancerIP,omitempty"` + + // Annotations specifies zero or more key/value pairs to set as annotations on the provisioned Service. + // + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + +// CredentialIssuerStatus describes the status of the Concierge. type CredentialIssuerStatus struct { // List of integration strategies that were attempted by Pinniped. Strategies []CredentialIssuerStrategy `json:"strategies"` @@ -47,7 +137,8 @@ type CredentialIssuerStatus struct { KubeConfigInfo *CredentialIssuerKubeConfigInfo `json:"kubeConfigInfo,omitempty"` } -// Information needed to form a valid Pinniped-based kubeconfig using this credential issuer. +// CredentialIssuerKubeConfigInfo provides the information needed to form a valid Pinniped-based kubeconfig using this credential issuer. +// This type is deprecated and will be removed in a future version. type CredentialIssuerKubeConfigInfo struct { // The K8s API server URL. // +kubebuilder:validation:MinLength=1 @@ -59,7 +150,7 @@ type CredentialIssuerKubeConfigInfo struct { CertificateAuthorityData string `json:"certificateAuthorityData"` } -// Status of an integration strategy that was attempted by Pinniped. +// CredentialIssuerStrategy describes the status of an integration strategy that was attempted by Pinniped. type CredentialIssuerStrategy struct { // Type of integration attempted. Type StrategyType `json:"type"` @@ -81,6 +172,7 @@ type CredentialIssuerStrategy struct { Frontend *CredentialIssuerFrontend `json:"frontend,omitempty"` } +// CredentialIssuerFrontend describes how to connect using a particular integration strategy. type CredentialIssuerFrontend struct { // Type describes which frontend mechanism clients can use with a strategy. Type FrontendType `json:"type"` @@ -118,7 +210,7 @@ type ImpersonationProxyInfo struct { CertificateAuthorityData string `json:"certificateAuthorityData"` } -// Describes the configuration status of a Pinniped credential issuer. +// CredentialIssuer describes the configuration and status of the Pinniped Concierge credential issuer. // +genclient // +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -128,12 +220,18 @@ type CredentialIssuer struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - // Status of the credential issuer. + // Spec describes the intended configuration of the Concierge. + // + // +optional + Spec CredentialIssuerSpec `json:"spec"` + + // CredentialIssuerStatus describes the status of the Concierge. + // // +optional Status CredentialIssuerStatus `json:"status"` } -// List of CredentialIssuer objects. +// CredentialIssuerList is a list of CredentialIssuer objects. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type CredentialIssuerList struct { metav1.TypeMeta `json:",inline"` diff --git a/generated/1.18/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.18/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go index 4a3ad605..cf679b2c 100644 --- a/generated/1.18/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.18/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go @@ -16,6 +16,7 @@ func (in *CredentialIssuer) DeepCopyInto(out *CredentialIssuer) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) return } @@ -113,6 +114,27 @@ func (in *CredentialIssuerList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CredentialIssuerSpec) DeepCopyInto(out *CredentialIssuerSpec) { + *out = *in + if in.ImpersonationProxy != nil { + in, out := &in.ImpersonationProxy, &out.ImpersonationProxy + *out = new(ImpersonationProxySpec) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialIssuerSpec. +func (in *CredentialIssuerSpec) DeepCopy() *CredentialIssuerSpec { + if in == nil { + return nil + } + out := new(CredentialIssuerSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CredentialIssuerStatus) DeepCopyInto(out *CredentialIssuerStatus) { *out = *in @@ -179,6 +201,46 @@ func (in *ImpersonationProxyInfo) DeepCopy() *ImpersonationProxyInfo { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImpersonationProxyServiceSpec) DeepCopyInto(out *ImpersonationProxyServiceSpec) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxyServiceSpec. +func (in *ImpersonationProxyServiceSpec) DeepCopy() *ImpersonationProxyServiceSpec { + if in == nil { + return nil + } + out := new(ImpersonationProxyServiceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImpersonationProxySpec) DeepCopyInto(out *ImpersonationProxySpec) { + *out = *in + in.Service.DeepCopyInto(&out.Service) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxySpec. +func (in *ImpersonationProxySpec) DeepCopy() *ImpersonationProxySpec { + if in == nil { + return nil + } + out := new(ImpersonationProxySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) { *out = *in diff --git a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0e28234d..74570a92 100644 --- a/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.18/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -68,8 +68,8 @@ type LDAPIdentityProviderGroupSearchAttributes struct { // GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP - // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, the default will act as if the GroupName were specified as "cn" (common name). + // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". + // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). // +optional GroupName string `json:"groupName,omitempty"` } diff --git a/generated/1.18/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.18/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8123f238..7b94b098 100644 --- a/generated/1.18/crds/config.concierge.pinniped.dev_credentialissuers.yaml +++ b/generated/1.18/crds/config.concierge.pinniped.dev_credentialissuers.yaml @@ -21,7 +21,8 @@ spec: - name: v1alpha1 schema: openAPIV3Schema: - description: Describes the configuration status of a Pinniped credential issuer. + description: CredentialIssuer describes the configuration and status of the + Pinniped Concierge credential issuer. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -35,8 +36,73 @@ spec: type: string metadata: type: object + spec: + description: Spec describes the intended configuration of the Concierge. + properties: + impersonationProxy: + description: ImpersonationProxy describes the intended configuration + of the Concierge impersonation proxy. + properties: + externalEndpoint: + description: "ExternalEndpoint describes the HTTPS endpoint where + the proxy will be exposed. If not set, the proxy will be served + using the external name of the LoadBalancer service or the cluster + service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.mode + is \"None\"." + type: string + mode: + description: 'Mode configures whether the impersonation proxy + should be started: - "disabled" explicitly disables the impersonation + proxy. This is the default. - "enabled" explicitly enables the + impersonation proxy. - "auto" enables or disables the impersonation + proxy based upon the cluster in which it is running.' + enum: + - auto + - enabled + - disabled + type: string + service: + default: + type: LoadBalancer + description: Service describes the configuration of the Service + provisioned to expose the impersonation proxy to clients. + properties: + annotations: + additionalProperties: + type: string + description: Annotations specifies zero or more key/value + pairs to set as annotations on the provisioned Service. + type: object + loadBalancerIP: + description: LoadBalancerIP specifies the IP address to set + in the spec.loadBalancerIP field of the provisioned Service. + This is not supported on all cloud providers. + maxLength: 255 + minLength: 1 + type: string + type: + default: LoadBalancer + description: "Type specifies the type of Service to provision + for the impersonation proxy. \n If the type is \"None\", + then the \"spec.impersonationProxy.externalEndpoint\" field + must be set to a non-empty value so that the Concierge can + properly advertise the endpoint in the CredentialIssuer's + status." + enum: + - LoadBalancer + - ClusterIP + - None + type: string + type: object + required: + - mode + - service + type: object + required: + - impersonationProxy + type: object status: - description: Status of the credential issuer. + description: CredentialIssuerStatus describes the status of the Concierge. properties: kubeConfigInfo: description: Information needed to form a valid Pinniped-based kubeconfig @@ -60,8 +126,8 @@ spec: description: List of integration strategies that were attempted by Pinniped. items: - description: Status of an integration strategy that was attempted - by Pinniped. + description: CredentialIssuerStrategy describes the status of an + integration strategy that was attempted by Pinniped. properties: frontend: description: Frontend describes how clients can connect using diff --git a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 46fbe1d0..ed9780b5 100644 --- a/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.18/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -86,10 +86,10 @@ spec: in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server - in the user's entry. Distinguished names can be used by - specifying lower-case "dn". Optional. When not specified, - the default will act as if the GroupName were specified - as "cn" (common name). + in the user's entry. E.g. "cn" for common name. Distinguished + names can be used by specifying lower-case "dn". Optional. + When not specified, the default will act as if the GroupName + were specified as "dn" (distinguished name). type: string type: object base: diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index 0e1f160b..48e0b834 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -220,7 +220,7 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge configuration [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-credentialissuer"] ==== CredentialIssuer -Describes the configuration status of a Pinniped credential issuer. +CredentialIssuer describes the configuration and status of the Pinniped Concierge credential issuer. .Appears In: **** @@ -232,7 +232,8 @@ Describes the configuration status of a Pinniped credential issuer. | Field | Description | *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. -| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-credentialissuerstatus[$$CredentialIssuerStatus$$]__ | Status of the credential issuer. +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-credentialissuerspec[$$CredentialIssuerSpec$$]__ | Spec describes the intended configuration of the Concierge. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-credentialissuerstatus[$$CredentialIssuerStatus$$]__ | CredentialIssuerStatus describes the status of the Concierge. |=== @@ -275,10 +276,27 @@ Describes the configuration status of a Pinniped credential issuer. +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-credentialissuerspec"] +==== CredentialIssuerSpec + +CredentialIssuerSpec describes the intended configuration of the Concierge. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-credentialissuer[$$CredentialIssuer$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`impersonationProxy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$]__ | ImpersonationProxy describes the intended configuration of the Concierge impersonation proxy. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-credentialissuerstatus"] ==== CredentialIssuerStatus -Status of a credential issuer. +CredentialIssuerStatus describes the status of the Concierge. .Appears In: **** @@ -333,6 +351,70 @@ Status of a credential issuer. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-impersonationproxymode"] +==== ImpersonationProxyMode (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-impersonationproxyservicespec"] +==== ImpersonationProxyServiceSpec + +ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __ImpersonationProxyServiceType__ | Type specifies the type of Service to provision for the impersonation proxy. + If the type is "None", then the "spec.impersonationProxy.externalEndpoint" field must be set to a non-empty value so that the Concierge can properly advertise the endpoint in the CredentialIssuer's status. +| *`loadBalancerIP`* __string__ | LoadBalancerIP specifies the IP address to set in the spec.loadBalancerIP field of the provisioned Service. This is not supported on all cloud providers. +| *`annotations`* __object (keys:string, values:string)__ | Annotations specifies zero or more key/value pairs to set as annotations on the provisioned Service. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-impersonationproxyservicetype"] +==== ImpersonationProxyServiceType (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-impersonationproxyservicespec[$$ImpersonationProxyServiceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-impersonationproxyspec"] +==== ImpersonationProxySpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-credentialissuerspec[$$CredentialIssuerSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`mode`* __ImpersonationProxyMode__ | Mode configures whether the impersonation proxy should be started: - "disabled" explicitly disables the impersonation proxy. This is the default. - "enabled" explicitly enables the impersonation proxy. - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running. +| *`service`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-impersonationproxyservicespec[$$ImpersonationProxyServiceSpec$$]__ | Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients. +| *`externalEndpoint`* __string__ | ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will be served using the external name of the LoadBalancer service or the cluster service DNS name. + This field must be non-empty when spec.impersonationProxy.service.mode is "None". +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo"] ==== TokenCredentialRequestAPIInfo @@ -770,7 +852,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "cn" (common name). +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). |=== diff --git a/generated/1.19/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/1.19/apis/concierge/config/v1alpha1/types_credentialissuer.go index 3c0c8ba0..60edeccc 100644 --- a/generated/1.19/apis/concierge/config/v1alpha1/types_credentialissuer.go +++ b/generated/1.19/apis/concierge/config/v1alpha1/types_credentialissuer.go @@ -3,17 +3,23 @@ package v1alpha1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) +// StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. // +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy type StrategyType string +// FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. // +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy type FrontendType string +// StrategyStatus enumerates whether a strategy is working on a cluster. // +kubebuilder:validation:Enum=Success;Error type StrategyStatus string +// StrategyReason enumerates the detailed reason why a strategy is in a particular status. // +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey type StrategyReason string @@ -36,7 +42,91 @@ const ( FetchedKeyStrategyReason = StrategyReason("FetchedKey") ) -// Status of a credential issuer. +// CredentialIssuerSpec describes the intended configuration of the Concierge. +type CredentialIssuerSpec struct { + // ImpersonationProxy describes the intended configuration of the Concierge impersonation proxy. + ImpersonationProxy *ImpersonationProxySpec `json:"impersonationProxy"` +} + +// ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// +// +kubebuilder:validation:Enum=auto;enabled;disabled +type ImpersonationProxyMode string + +const ( + // ImpersonationProxyModeDisabled explicitly disables the impersonation proxy. + ImpersonationProxyModeDisabled = ImpersonationProxyMode("disabled") + + // ImpersonationProxyModeEnabled explicitly enables the impersonation proxy. + ImpersonationProxyModeEnabled = ImpersonationProxyMode("enabled") + + // ImpersonationProxyModeAuto enables or disables the impersonation proxy based upon the cluster in which it is running. + ImpersonationProxyModeAuto = ImpersonationProxyMode("auto") +) + +// ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// +// +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None +type ImpersonationProxyServiceType string + +const ( + // ImpersonationProxyServiceTypeLoadBalancer provisions a service of type LoadBalancer. + ImpersonationProxyServiceTypeLoadBalancer = ImpersonationProxyServiceType("LoadBalancer") + + // ImpersonationProxyServiceTypeClusterIP provisions a service of type ClusterIP. + ImpersonationProxyServiceTypeClusterIP = ImpersonationProxyServiceType("ClusterIP") + + // ImpersonationProxyServiceTypeNone does not automatically provision any service. + ImpersonationProxyServiceTypeNone = ImpersonationProxyServiceType("None") +) + +// ImpersonationProxySpec describes the intended configuration of the Concierge impersonation proxy. +type ImpersonationProxySpec struct { + // Mode configures whether the impersonation proxy should be started: + // - "disabled" explicitly disables the impersonation proxy. This is the default. + // - "enabled" explicitly enables the impersonation proxy. + // - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running. + Mode ImpersonationProxyMode `json:"mode"` + + // Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients. + // + // +kubebuilder:default:={"type": "LoadBalancer"} + Service ImpersonationProxyServiceSpec `json:"service"` + + // ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will + // be served using the external name of the LoadBalancer service or the cluster service DNS name. + // + // This field must be non-empty when spec.impersonationProxy.service.mode is "None". + // + // +optional + ExternalEndpoint string `json:"externalEndpoint,omitempty"` +} + +// ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy. +type ImpersonationProxyServiceSpec struct { + // Type specifies the type of Service to provision for the impersonation proxy. + // + // If the type is "None", then the "spec.impersonationProxy.externalEndpoint" field must be set to a non-empty + // value so that the Concierge can properly advertise the endpoint in the CredentialIssuer's status. + // + // +kubebuilder:default:="LoadBalancer" + Type ImpersonationProxyServiceType `json:"type,omitempty"` + + // LoadBalancerIP specifies the IP address to set in the spec.loadBalancerIP field of the provisioned Service. + // This is not supported on all cloud providers. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=255 + // +optional + LoadBalancerIP string `json:"loadBalancerIP,omitempty"` + + // Annotations specifies zero or more key/value pairs to set as annotations on the provisioned Service. + // + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + +// CredentialIssuerStatus describes the status of the Concierge. type CredentialIssuerStatus struct { // List of integration strategies that were attempted by Pinniped. Strategies []CredentialIssuerStrategy `json:"strategies"` @@ -47,7 +137,8 @@ type CredentialIssuerStatus struct { KubeConfigInfo *CredentialIssuerKubeConfigInfo `json:"kubeConfigInfo,omitempty"` } -// Information needed to form a valid Pinniped-based kubeconfig using this credential issuer. +// CredentialIssuerKubeConfigInfo provides the information needed to form a valid Pinniped-based kubeconfig using this credential issuer. +// This type is deprecated and will be removed in a future version. type CredentialIssuerKubeConfigInfo struct { // The K8s API server URL. // +kubebuilder:validation:MinLength=1 @@ -59,7 +150,7 @@ type CredentialIssuerKubeConfigInfo struct { CertificateAuthorityData string `json:"certificateAuthorityData"` } -// Status of an integration strategy that was attempted by Pinniped. +// CredentialIssuerStrategy describes the status of an integration strategy that was attempted by Pinniped. type CredentialIssuerStrategy struct { // Type of integration attempted. Type StrategyType `json:"type"` @@ -81,6 +172,7 @@ type CredentialIssuerStrategy struct { Frontend *CredentialIssuerFrontend `json:"frontend,omitempty"` } +// CredentialIssuerFrontend describes how to connect using a particular integration strategy. type CredentialIssuerFrontend struct { // Type describes which frontend mechanism clients can use with a strategy. Type FrontendType `json:"type"` @@ -118,7 +210,7 @@ type ImpersonationProxyInfo struct { CertificateAuthorityData string `json:"certificateAuthorityData"` } -// Describes the configuration status of a Pinniped credential issuer. +// CredentialIssuer describes the configuration and status of the Pinniped Concierge credential issuer. // +genclient // +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -128,12 +220,18 @@ type CredentialIssuer struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - // Status of the credential issuer. + // Spec describes the intended configuration of the Concierge. + // + // +optional + Spec CredentialIssuerSpec `json:"spec"` + + // CredentialIssuerStatus describes the status of the Concierge. + // // +optional Status CredentialIssuerStatus `json:"status"` } -// List of CredentialIssuer objects. +// CredentialIssuerList is a list of CredentialIssuer objects. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type CredentialIssuerList struct { metav1.TypeMeta `json:",inline"` diff --git a/generated/1.19/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.19/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go index 4a3ad605..cf679b2c 100644 --- a/generated/1.19/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.19/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go @@ -16,6 +16,7 @@ func (in *CredentialIssuer) DeepCopyInto(out *CredentialIssuer) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) return } @@ -113,6 +114,27 @@ func (in *CredentialIssuerList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CredentialIssuerSpec) DeepCopyInto(out *CredentialIssuerSpec) { + *out = *in + if in.ImpersonationProxy != nil { + in, out := &in.ImpersonationProxy, &out.ImpersonationProxy + *out = new(ImpersonationProxySpec) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialIssuerSpec. +func (in *CredentialIssuerSpec) DeepCopy() *CredentialIssuerSpec { + if in == nil { + return nil + } + out := new(CredentialIssuerSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CredentialIssuerStatus) DeepCopyInto(out *CredentialIssuerStatus) { *out = *in @@ -179,6 +201,46 @@ func (in *ImpersonationProxyInfo) DeepCopy() *ImpersonationProxyInfo { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImpersonationProxyServiceSpec) DeepCopyInto(out *ImpersonationProxyServiceSpec) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxyServiceSpec. +func (in *ImpersonationProxyServiceSpec) DeepCopy() *ImpersonationProxyServiceSpec { + if in == nil { + return nil + } + out := new(ImpersonationProxyServiceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImpersonationProxySpec) DeepCopyInto(out *ImpersonationProxySpec) { + *out = *in + in.Service.DeepCopyInto(&out.Service) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxySpec. +func (in *ImpersonationProxySpec) DeepCopy() *ImpersonationProxySpec { + if in == nil { + return nil + } + out := new(ImpersonationProxySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) { *out = *in diff --git a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0e28234d..74570a92 100644 --- a/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.19/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -68,8 +68,8 @@ type LDAPIdentityProviderGroupSearchAttributes struct { // GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP - // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, the default will act as if the GroupName were specified as "cn" (common name). + // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". + // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). // +optional GroupName string `json:"groupName,omitempty"` } diff --git a/generated/1.19/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.19/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8123f238..7b94b098 100644 --- a/generated/1.19/crds/config.concierge.pinniped.dev_credentialissuers.yaml +++ b/generated/1.19/crds/config.concierge.pinniped.dev_credentialissuers.yaml @@ -21,7 +21,8 @@ spec: - name: v1alpha1 schema: openAPIV3Schema: - description: Describes the configuration status of a Pinniped credential issuer. + description: CredentialIssuer describes the configuration and status of the + Pinniped Concierge credential issuer. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -35,8 +36,73 @@ spec: type: string metadata: type: object + spec: + description: Spec describes the intended configuration of the Concierge. + properties: + impersonationProxy: + description: ImpersonationProxy describes the intended configuration + of the Concierge impersonation proxy. + properties: + externalEndpoint: + description: "ExternalEndpoint describes the HTTPS endpoint where + the proxy will be exposed. If not set, the proxy will be served + using the external name of the LoadBalancer service or the cluster + service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.mode + is \"None\"." + type: string + mode: + description: 'Mode configures whether the impersonation proxy + should be started: - "disabled" explicitly disables the impersonation + proxy. This is the default. - "enabled" explicitly enables the + impersonation proxy. - "auto" enables or disables the impersonation + proxy based upon the cluster in which it is running.' + enum: + - auto + - enabled + - disabled + type: string + service: + default: + type: LoadBalancer + description: Service describes the configuration of the Service + provisioned to expose the impersonation proxy to clients. + properties: + annotations: + additionalProperties: + type: string + description: Annotations specifies zero or more key/value + pairs to set as annotations on the provisioned Service. + type: object + loadBalancerIP: + description: LoadBalancerIP specifies the IP address to set + in the spec.loadBalancerIP field of the provisioned Service. + This is not supported on all cloud providers. + maxLength: 255 + minLength: 1 + type: string + type: + default: LoadBalancer + description: "Type specifies the type of Service to provision + for the impersonation proxy. \n If the type is \"None\", + then the \"spec.impersonationProxy.externalEndpoint\" field + must be set to a non-empty value so that the Concierge can + properly advertise the endpoint in the CredentialIssuer's + status." + enum: + - LoadBalancer + - ClusterIP + - None + type: string + type: object + required: + - mode + - service + type: object + required: + - impersonationProxy + type: object status: - description: Status of the credential issuer. + description: CredentialIssuerStatus describes the status of the Concierge. properties: kubeConfigInfo: description: Information needed to form a valid Pinniped-based kubeconfig @@ -60,8 +126,8 @@ spec: description: List of integration strategies that were attempted by Pinniped. items: - description: Status of an integration strategy that was attempted - by Pinniped. + description: CredentialIssuerStrategy describes the status of an + integration strategy that was attempted by Pinniped. properties: frontend: description: Frontend describes how clients can connect using diff --git a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 46fbe1d0..ed9780b5 100644 --- a/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.19/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -86,10 +86,10 @@ spec: in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server - in the user's entry. Distinguished names can be used by - specifying lower-case "dn". Optional. When not specified, - the default will act as if the GroupName were specified - as "cn" (common name). + in the user's entry. E.g. "cn" for common name. Distinguished + names can be used by specifying lower-case "dn". Optional. + When not specified, the default will act as if the GroupName + were specified as "dn" (distinguished name). type: string type: object base: diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index 103b809c..4df9c3d1 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -220,7 +220,7 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped concierge configuration [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-credentialissuer"] ==== CredentialIssuer -Describes the configuration status of a Pinniped credential issuer. +CredentialIssuer describes the configuration and status of the Pinniped Concierge credential issuer. .Appears In: **** @@ -232,7 +232,8 @@ Describes the configuration status of a Pinniped credential issuer. | Field | Description | *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.2/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`. -| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-credentialissuerstatus[$$CredentialIssuerStatus$$]__ | Status of the credential issuer. +| *`spec`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-credentialissuerspec[$$CredentialIssuerSpec$$]__ | Spec describes the intended configuration of the Concierge. +| *`status`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-credentialissuerstatus[$$CredentialIssuerStatus$$]__ | CredentialIssuerStatus describes the status of the Concierge. |=== @@ -275,10 +276,27 @@ Describes the configuration status of a Pinniped credential issuer. +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-credentialissuerspec"] +==== CredentialIssuerSpec + +CredentialIssuerSpec describes the intended configuration of the Concierge. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-credentialissuer[$$CredentialIssuer$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`impersonationProxy`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$]__ | ImpersonationProxy describes the intended configuration of the Concierge impersonation proxy. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-credentialissuerstatus"] ==== CredentialIssuerStatus -Status of a credential issuer. +CredentialIssuerStatus describes the status of the Concierge. .Appears In: **** @@ -333,6 +351,70 @@ Status of a credential issuer. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-impersonationproxymode"] +==== ImpersonationProxyMode (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-impersonationproxyservicespec"] +==== ImpersonationProxyServiceSpec + +ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-impersonationproxyspec[$$ImpersonationProxySpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __ImpersonationProxyServiceType__ | Type specifies the type of Service to provision for the impersonation proxy. + If the type is "None", then the "spec.impersonationProxy.externalEndpoint" field must be set to a non-empty value so that the Concierge can properly advertise the endpoint in the CredentialIssuer's status. +| *`loadBalancerIP`* __string__ | LoadBalancerIP specifies the IP address to set in the spec.loadBalancerIP field of the provisioned Service. This is not supported on all cloud providers. +| *`annotations`* __object (keys:string, values:string)__ | Annotations specifies zero or more key/value pairs to set as annotations on the provisioned Service. +|=== + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-impersonationproxyservicetype"] +==== ImpersonationProxyServiceType (string) + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-impersonationproxyservicespec[$$ImpersonationProxyServiceSpec$$] +**** + + + +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-impersonationproxyspec"] +==== ImpersonationProxySpec + + + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-credentialissuerspec[$$CredentialIssuerSpec$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`mode`* __ImpersonationProxyMode__ | Mode configures whether the impersonation proxy should be started: - "disabled" explicitly disables the impersonation proxy. This is the default. - "enabled" explicitly enables the impersonation proxy. - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running. +| *`service`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-impersonationproxyservicespec[$$ImpersonationProxyServiceSpec$$]__ | Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients. +| *`externalEndpoint`* __string__ | ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will be served using the external name of the LoadBalancer service or the cluster service DNS name. + This field must be non-empty when spec.impersonationProxy.service.mode is "None". +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-concierge-config-v1alpha1-tokencredentialrequestapiinfo"] ==== TokenCredentialRequestAPIInfo @@ -770,7 +852,7 @@ LDAPIdentityProvider describes the configuration of an upstream Lightweight Dire [cols="25a,75a", options="header"] |=== | Field | Description -| *`groupName`* __string__ | GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "cn" (common name). +| *`groupName`* __string__ | GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). |=== diff --git a/generated/1.20/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/1.20/apis/concierge/config/v1alpha1/types_credentialissuer.go index 3c0c8ba0..60edeccc 100644 --- a/generated/1.20/apis/concierge/config/v1alpha1/types_credentialissuer.go +++ b/generated/1.20/apis/concierge/config/v1alpha1/types_credentialissuer.go @@ -3,17 +3,23 @@ package v1alpha1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) +// StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. // +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy type StrategyType string +// FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. // +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy type FrontendType string +// StrategyStatus enumerates whether a strategy is working on a cluster. // +kubebuilder:validation:Enum=Success;Error type StrategyStatus string +// StrategyReason enumerates the detailed reason why a strategy is in a particular status. // +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey type StrategyReason string @@ -36,7 +42,91 @@ const ( FetchedKeyStrategyReason = StrategyReason("FetchedKey") ) -// Status of a credential issuer. +// CredentialIssuerSpec describes the intended configuration of the Concierge. +type CredentialIssuerSpec struct { + // ImpersonationProxy describes the intended configuration of the Concierge impersonation proxy. + ImpersonationProxy *ImpersonationProxySpec `json:"impersonationProxy"` +} + +// ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// +// +kubebuilder:validation:Enum=auto;enabled;disabled +type ImpersonationProxyMode string + +const ( + // ImpersonationProxyModeDisabled explicitly disables the impersonation proxy. + ImpersonationProxyModeDisabled = ImpersonationProxyMode("disabled") + + // ImpersonationProxyModeEnabled explicitly enables the impersonation proxy. + ImpersonationProxyModeEnabled = ImpersonationProxyMode("enabled") + + // ImpersonationProxyModeAuto enables or disables the impersonation proxy based upon the cluster in which it is running. + ImpersonationProxyModeAuto = ImpersonationProxyMode("auto") +) + +// ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// +// +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None +type ImpersonationProxyServiceType string + +const ( + // ImpersonationProxyServiceTypeLoadBalancer provisions a service of type LoadBalancer. + ImpersonationProxyServiceTypeLoadBalancer = ImpersonationProxyServiceType("LoadBalancer") + + // ImpersonationProxyServiceTypeClusterIP provisions a service of type ClusterIP. + ImpersonationProxyServiceTypeClusterIP = ImpersonationProxyServiceType("ClusterIP") + + // ImpersonationProxyServiceTypeNone does not automatically provision any service. + ImpersonationProxyServiceTypeNone = ImpersonationProxyServiceType("None") +) + +// ImpersonationProxySpec describes the intended configuration of the Concierge impersonation proxy. +type ImpersonationProxySpec struct { + // Mode configures whether the impersonation proxy should be started: + // - "disabled" explicitly disables the impersonation proxy. This is the default. + // - "enabled" explicitly enables the impersonation proxy. + // - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running. + Mode ImpersonationProxyMode `json:"mode"` + + // Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients. + // + // +kubebuilder:default:={"type": "LoadBalancer"} + Service ImpersonationProxyServiceSpec `json:"service"` + + // ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will + // be served using the external name of the LoadBalancer service or the cluster service DNS name. + // + // This field must be non-empty when spec.impersonationProxy.service.mode is "None". + // + // +optional + ExternalEndpoint string `json:"externalEndpoint,omitempty"` +} + +// ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy. +type ImpersonationProxyServiceSpec struct { + // Type specifies the type of Service to provision for the impersonation proxy. + // + // If the type is "None", then the "spec.impersonationProxy.externalEndpoint" field must be set to a non-empty + // value so that the Concierge can properly advertise the endpoint in the CredentialIssuer's status. + // + // +kubebuilder:default:="LoadBalancer" + Type ImpersonationProxyServiceType `json:"type,omitempty"` + + // LoadBalancerIP specifies the IP address to set in the spec.loadBalancerIP field of the provisioned Service. + // This is not supported on all cloud providers. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=255 + // +optional + LoadBalancerIP string `json:"loadBalancerIP,omitempty"` + + // Annotations specifies zero or more key/value pairs to set as annotations on the provisioned Service. + // + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + +// CredentialIssuerStatus describes the status of the Concierge. type CredentialIssuerStatus struct { // List of integration strategies that were attempted by Pinniped. Strategies []CredentialIssuerStrategy `json:"strategies"` @@ -47,7 +137,8 @@ type CredentialIssuerStatus struct { KubeConfigInfo *CredentialIssuerKubeConfigInfo `json:"kubeConfigInfo,omitempty"` } -// Information needed to form a valid Pinniped-based kubeconfig using this credential issuer. +// CredentialIssuerKubeConfigInfo provides the information needed to form a valid Pinniped-based kubeconfig using this credential issuer. +// This type is deprecated and will be removed in a future version. type CredentialIssuerKubeConfigInfo struct { // The K8s API server URL. // +kubebuilder:validation:MinLength=1 @@ -59,7 +150,7 @@ type CredentialIssuerKubeConfigInfo struct { CertificateAuthorityData string `json:"certificateAuthorityData"` } -// Status of an integration strategy that was attempted by Pinniped. +// CredentialIssuerStrategy describes the status of an integration strategy that was attempted by Pinniped. type CredentialIssuerStrategy struct { // Type of integration attempted. Type StrategyType `json:"type"` @@ -81,6 +172,7 @@ type CredentialIssuerStrategy struct { Frontend *CredentialIssuerFrontend `json:"frontend,omitempty"` } +// CredentialIssuerFrontend describes how to connect using a particular integration strategy. type CredentialIssuerFrontend struct { // Type describes which frontend mechanism clients can use with a strategy. Type FrontendType `json:"type"` @@ -118,7 +210,7 @@ type ImpersonationProxyInfo struct { CertificateAuthorityData string `json:"certificateAuthorityData"` } -// Describes the configuration status of a Pinniped credential issuer. +// CredentialIssuer describes the configuration and status of the Pinniped Concierge credential issuer. // +genclient // +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -128,12 +220,18 @@ type CredentialIssuer struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - // Status of the credential issuer. + // Spec describes the intended configuration of the Concierge. + // + // +optional + Spec CredentialIssuerSpec `json:"spec"` + + // CredentialIssuerStatus describes the status of the Concierge. + // // +optional Status CredentialIssuerStatus `json:"status"` } -// List of CredentialIssuer objects. +// CredentialIssuerList is a list of CredentialIssuer objects. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type CredentialIssuerList struct { metav1.TypeMeta `json:",inline"` diff --git a/generated/1.20/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.20/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go index 4a3ad605..cf679b2c 100644 --- a/generated/1.20/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.20/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go @@ -16,6 +16,7 @@ func (in *CredentialIssuer) DeepCopyInto(out *CredentialIssuer) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) return } @@ -113,6 +114,27 @@ func (in *CredentialIssuerList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CredentialIssuerSpec) DeepCopyInto(out *CredentialIssuerSpec) { + *out = *in + if in.ImpersonationProxy != nil { + in, out := &in.ImpersonationProxy, &out.ImpersonationProxy + *out = new(ImpersonationProxySpec) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialIssuerSpec. +func (in *CredentialIssuerSpec) DeepCopy() *CredentialIssuerSpec { + if in == nil { + return nil + } + out := new(CredentialIssuerSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CredentialIssuerStatus) DeepCopyInto(out *CredentialIssuerStatus) { *out = *in @@ -179,6 +201,46 @@ func (in *ImpersonationProxyInfo) DeepCopy() *ImpersonationProxyInfo { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImpersonationProxyServiceSpec) DeepCopyInto(out *ImpersonationProxyServiceSpec) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxyServiceSpec. +func (in *ImpersonationProxyServiceSpec) DeepCopy() *ImpersonationProxyServiceSpec { + if in == nil { + return nil + } + out := new(ImpersonationProxyServiceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImpersonationProxySpec) DeepCopyInto(out *ImpersonationProxySpec) { + *out = *in + in.Service.DeepCopyInto(&out.Service) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxySpec. +func (in *ImpersonationProxySpec) DeepCopy() *ImpersonationProxySpec { + if in == nil { + return nil + } + out := new(ImpersonationProxySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) { *out = *in diff --git a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0e28234d..74570a92 100644 --- a/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/1.20/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -68,8 +68,8 @@ type LDAPIdentityProviderGroupSearchAttributes struct { // GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP - // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, the default will act as if the GroupName were specified as "cn" (common name). + // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". + // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). // +optional GroupName string `json:"groupName,omitempty"` } diff --git a/generated/1.20/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.20/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8123f238..7b94b098 100644 --- a/generated/1.20/crds/config.concierge.pinniped.dev_credentialissuers.yaml +++ b/generated/1.20/crds/config.concierge.pinniped.dev_credentialissuers.yaml @@ -21,7 +21,8 @@ spec: - name: v1alpha1 schema: openAPIV3Schema: - description: Describes the configuration status of a Pinniped credential issuer. + description: CredentialIssuer describes the configuration and status of the + Pinniped Concierge credential issuer. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -35,8 +36,73 @@ spec: type: string metadata: type: object + spec: + description: Spec describes the intended configuration of the Concierge. + properties: + impersonationProxy: + description: ImpersonationProxy describes the intended configuration + of the Concierge impersonation proxy. + properties: + externalEndpoint: + description: "ExternalEndpoint describes the HTTPS endpoint where + the proxy will be exposed. If not set, the proxy will be served + using the external name of the LoadBalancer service or the cluster + service DNS name. \n This field must be non-empty when spec.impersonationProxy.service.mode + is \"None\"." + type: string + mode: + description: 'Mode configures whether the impersonation proxy + should be started: - "disabled" explicitly disables the impersonation + proxy. This is the default. - "enabled" explicitly enables the + impersonation proxy. - "auto" enables or disables the impersonation + proxy based upon the cluster in which it is running.' + enum: + - auto + - enabled + - disabled + type: string + service: + default: + type: LoadBalancer + description: Service describes the configuration of the Service + provisioned to expose the impersonation proxy to clients. + properties: + annotations: + additionalProperties: + type: string + description: Annotations specifies zero or more key/value + pairs to set as annotations on the provisioned Service. + type: object + loadBalancerIP: + description: LoadBalancerIP specifies the IP address to set + in the spec.loadBalancerIP field of the provisioned Service. + This is not supported on all cloud providers. + maxLength: 255 + minLength: 1 + type: string + type: + default: LoadBalancer + description: "Type specifies the type of Service to provision + for the impersonation proxy. \n If the type is \"None\", + then the \"spec.impersonationProxy.externalEndpoint\" field + must be set to a non-empty value so that the Concierge can + properly advertise the endpoint in the CredentialIssuer's + status." + enum: + - LoadBalancer + - ClusterIP + - None + type: string + type: object + required: + - mode + - service + type: object + required: + - impersonationProxy + type: object status: - description: Status of the credential issuer. + description: CredentialIssuerStatus describes the status of the Concierge. properties: kubeConfigInfo: description: Information needed to form a valid Pinniped-based kubeconfig @@ -60,8 +126,8 @@ spec: description: List of integration strategies that were attempted by Pinniped. items: - description: Status of an integration strategy that was attempted - by Pinniped. + description: CredentialIssuerStrategy describes the status of an + integration strategy that was attempted by Pinniped. properties: frontend: description: Frontend describes how clients can connect using diff --git a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml index 46fbe1d0..ed9780b5 100644 --- a/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml +++ b/generated/1.20/crds/idp.supervisor.pinniped.dev_ldapidentityproviders.yaml @@ -86,10 +86,10 @@ spec: in the user's list of groups after a successful authentication. The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP server - in the user's entry. Distinguished names can be used by - specifying lower-case "dn". Optional. When not specified, - the default will act as if the GroupName were specified - as "cn" (common name). + in the user's entry. E.g. "cn" for common name. Distinguished + names can be used by specifying lower-case "dn". Optional. + When not specified, the default will act as if the GroupName + were specified as "dn" (distinguished name). type: string type: object base: diff --git a/generated/latest/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/latest/apis/concierge/config/v1alpha1/types_credentialissuer.go index 3c0c8ba0..60edeccc 100644 --- a/generated/latest/apis/concierge/config/v1alpha1/types_credentialissuer.go +++ b/generated/latest/apis/concierge/config/v1alpha1/types_credentialissuer.go @@ -3,17 +3,23 @@ package v1alpha1 -import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) +// StrategyType enumerates a type of "strategy" used to implement credential access on a cluster. // +kubebuilder:validation:Enum=KubeClusterSigningCertificate;ImpersonationProxy type StrategyType string +// FrontendType enumerates a type of "frontend" used to provide access to users of a cluster. // +kubebuilder:validation:Enum=TokenCredentialRequestAPI;ImpersonationProxy type FrontendType string +// StrategyStatus enumerates whether a strategy is working on a cluster. // +kubebuilder:validation:Enum=Success;Error type StrategyStatus string +// StrategyReason enumerates the detailed reason why a strategy is in a particular status. // +kubebuilder:validation:Enum=Listening;Pending;Disabled;ErrorDuringSetup;CouldNotFetchKey;CouldNotGetClusterInfo;FetchedKey type StrategyReason string @@ -36,7 +42,91 @@ const ( FetchedKeyStrategyReason = StrategyReason("FetchedKey") ) -// Status of a credential issuer. +// CredentialIssuerSpec describes the intended configuration of the Concierge. +type CredentialIssuerSpec struct { + // ImpersonationProxy describes the intended configuration of the Concierge impersonation proxy. + ImpersonationProxy *ImpersonationProxySpec `json:"impersonationProxy"` +} + +// ImpersonationProxyMode enumerates the configuration modes for the impersonation proxy. +// +// +kubebuilder:validation:Enum=auto;enabled;disabled +type ImpersonationProxyMode string + +const ( + // ImpersonationProxyModeDisabled explicitly disables the impersonation proxy. + ImpersonationProxyModeDisabled = ImpersonationProxyMode("disabled") + + // ImpersonationProxyModeEnabled explicitly enables the impersonation proxy. + ImpersonationProxyModeEnabled = ImpersonationProxyMode("enabled") + + // ImpersonationProxyModeAuto enables or disables the impersonation proxy based upon the cluster in which it is running. + ImpersonationProxyModeAuto = ImpersonationProxyMode("auto") +) + +// ImpersonationProxyServiceType enumerates the types of service that can be provisioned for the impersonation proxy. +// +// +kubebuilder:validation:Enum=LoadBalancer;ClusterIP;None +type ImpersonationProxyServiceType string + +const ( + // ImpersonationProxyServiceTypeLoadBalancer provisions a service of type LoadBalancer. + ImpersonationProxyServiceTypeLoadBalancer = ImpersonationProxyServiceType("LoadBalancer") + + // ImpersonationProxyServiceTypeClusterIP provisions a service of type ClusterIP. + ImpersonationProxyServiceTypeClusterIP = ImpersonationProxyServiceType("ClusterIP") + + // ImpersonationProxyServiceTypeNone does not automatically provision any service. + ImpersonationProxyServiceTypeNone = ImpersonationProxyServiceType("None") +) + +// ImpersonationProxySpec describes the intended configuration of the Concierge impersonation proxy. +type ImpersonationProxySpec struct { + // Mode configures whether the impersonation proxy should be started: + // - "disabled" explicitly disables the impersonation proxy. This is the default. + // - "enabled" explicitly enables the impersonation proxy. + // - "auto" enables or disables the impersonation proxy based upon the cluster in which it is running. + Mode ImpersonationProxyMode `json:"mode"` + + // Service describes the configuration of the Service provisioned to expose the impersonation proxy to clients. + // + // +kubebuilder:default:={"type": "LoadBalancer"} + Service ImpersonationProxyServiceSpec `json:"service"` + + // ExternalEndpoint describes the HTTPS endpoint where the proxy will be exposed. If not set, the proxy will + // be served using the external name of the LoadBalancer service or the cluster service DNS name. + // + // This field must be non-empty when spec.impersonationProxy.service.mode is "None". + // + // +optional + ExternalEndpoint string `json:"externalEndpoint,omitempty"` +} + +// ImpersonationProxyServiceSpec describes how the Concierge should provision a Service to expose the impersonation proxy. +type ImpersonationProxyServiceSpec struct { + // Type specifies the type of Service to provision for the impersonation proxy. + // + // If the type is "None", then the "spec.impersonationProxy.externalEndpoint" field must be set to a non-empty + // value so that the Concierge can properly advertise the endpoint in the CredentialIssuer's status. + // + // +kubebuilder:default:="LoadBalancer" + Type ImpersonationProxyServiceType `json:"type,omitempty"` + + // LoadBalancerIP specifies the IP address to set in the spec.loadBalancerIP field of the provisioned Service. + // This is not supported on all cloud providers. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=255 + // +optional + LoadBalancerIP string `json:"loadBalancerIP,omitempty"` + + // Annotations specifies zero or more key/value pairs to set as annotations on the provisioned Service. + // + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + +// CredentialIssuerStatus describes the status of the Concierge. type CredentialIssuerStatus struct { // List of integration strategies that were attempted by Pinniped. Strategies []CredentialIssuerStrategy `json:"strategies"` @@ -47,7 +137,8 @@ type CredentialIssuerStatus struct { KubeConfigInfo *CredentialIssuerKubeConfigInfo `json:"kubeConfigInfo,omitempty"` } -// Information needed to form a valid Pinniped-based kubeconfig using this credential issuer. +// CredentialIssuerKubeConfigInfo provides the information needed to form a valid Pinniped-based kubeconfig using this credential issuer. +// This type is deprecated and will be removed in a future version. type CredentialIssuerKubeConfigInfo struct { // The K8s API server URL. // +kubebuilder:validation:MinLength=1 @@ -59,7 +150,7 @@ type CredentialIssuerKubeConfigInfo struct { CertificateAuthorityData string `json:"certificateAuthorityData"` } -// Status of an integration strategy that was attempted by Pinniped. +// CredentialIssuerStrategy describes the status of an integration strategy that was attempted by Pinniped. type CredentialIssuerStrategy struct { // Type of integration attempted. Type StrategyType `json:"type"` @@ -81,6 +172,7 @@ type CredentialIssuerStrategy struct { Frontend *CredentialIssuerFrontend `json:"frontend,omitempty"` } +// CredentialIssuerFrontend describes how to connect using a particular integration strategy. type CredentialIssuerFrontend struct { // Type describes which frontend mechanism clients can use with a strategy. Type FrontendType `json:"type"` @@ -118,7 +210,7 @@ type ImpersonationProxyInfo struct { CertificateAuthorityData string `json:"certificateAuthorityData"` } -// Describes the configuration status of a Pinniped credential issuer. +// CredentialIssuer describes the configuration and status of the Pinniped Concierge credential issuer. // +genclient // +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -128,12 +220,18 @@ type CredentialIssuer struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - // Status of the credential issuer. + // Spec describes the intended configuration of the Concierge. + // + // +optional + Spec CredentialIssuerSpec `json:"spec"` + + // CredentialIssuerStatus describes the status of the Concierge. + // // +optional Status CredentialIssuerStatus `json:"status"` } -// List of CredentialIssuer objects. +// CredentialIssuerList is a list of CredentialIssuer objects. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type CredentialIssuerList struct { metav1.TypeMeta `json:",inline"` diff --git a/generated/latest/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go index 4a3ad605..cf679b2c 100644 --- a/generated/latest/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/concierge/config/v1alpha1/zz_generated.deepcopy.go @@ -16,6 +16,7 @@ func (in *CredentialIssuer) DeepCopyInto(out *CredentialIssuer) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) return } @@ -113,6 +114,27 @@ func (in *CredentialIssuerList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CredentialIssuerSpec) DeepCopyInto(out *CredentialIssuerSpec) { + *out = *in + if in.ImpersonationProxy != nil { + in, out := &in.ImpersonationProxy, &out.ImpersonationProxy + *out = new(ImpersonationProxySpec) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialIssuerSpec. +func (in *CredentialIssuerSpec) DeepCopy() *CredentialIssuerSpec { + if in == nil { + return nil + } + out := new(CredentialIssuerSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CredentialIssuerStatus) DeepCopyInto(out *CredentialIssuerStatus) { *out = *in @@ -179,6 +201,46 @@ func (in *ImpersonationProxyInfo) DeepCopy() *ImpersonationProxyInfo { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImpersonationProxyServiceSpec) DeepCopyInto(out *ImpersonationProxyServiceSpec) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxyServiceSpec. +func (in *ImpersonationProxyServiceSpec) DeepCopy() *ImpersonationProxyServiceSpec { + if in == nil { + return nil + } + out := new(ImpersonationProxyServiceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImpersonationProxySpec) DeepCopyInto(out *ImpersonationProxySpec) { + *out = *in + in.Service.DeepCopyInto(&out.Service) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImpersonationProxySpec. +func (in *ImpersonationProxySpec) DeepCopy() *ImpersonationProxySpec { + if in == nil { + return nil + } + out := new(ImpersonationProxySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TokenCredentialRequestAPIInfo) DeepCopyInto(out *TokenCredentialRequestAPIInfo) { *out = *in diff --git a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go index 0e28234d..74570a92 100644 --- a/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go +++ b/generated/latest/apis/supervisor/idp/v1alpha1/types_ldapidentityprovider.go @@ -68,8 +68,8 @@ type LDAPIdentityProviderGroupSearchAttributes struct { // GroupName specifies the name of the attribute in the LDAP entries whose value shall become a group name // in the user's list of groups after a successful authentication. // The value of this field is case-sensitive and must match the case of the attribute name returned by the LDAP - // server in the user's entry. Distinguished names can be used by specifying lower-case "dn". - // Optional. When not specified, the default will act as if the GroupName were specified as "cn" (common name). + // server in the user's entry. E.g. "cn" for common name. Distinguished names can be used by specifying lower-case "dn". + // Optional. When not specified, the default will act as if the GroupName were specified as "dn" (distinguished name). // +optional GroupName string `json:"groupName,omitempty"` } diff --git a/go.mod b/go.mod index 82b6a2a0..929469f7 100644 --- a/go.mod +++ b/go.mod @@ -17,9 +17,8 @@ require ( github.com/google/gofuzz v1.2.0 github.com/gorilla/securecookie v1.1.1 github.com/gorilla/websocket v1.4.2 - github.com/oleiade/reflections v1.0.1 // indirect github.com/onsi/ginkgo v1.13.0 // indirect - github.com/ory/fosite v0.39.0 + github.com/ory/fosite v0.40.2 github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 github.com/pkg/errors v0.9.1 github.com/sclevine/agouti v3.0.0+incompatible @@ -39,7 +38,7 @@ require ( k8s.io/client-go v0.21.1 k8s.io/component-base v0.21.1 k8s.io/gengo v0.0.0-20210203185629-de9496dff47b - k8s.io/klog/v2 v2.8.0 + k8s.io/klog/v2 v2.9.0 k8s.io/kube-aggregator v0.21.1 k8s.io/utils v0.0.0-20210521133846-da695404a2bc sigs.k8s.io/yaml v1.2.0 @@ -56,3 +55,10 @@ replace github.com/oleiade/reflections v1.0.0 => github.com/oleiade/reflections // We use the SHA of github.com/form3tech-oss/jwt-go@v3.2.2 to get around "used for two different module paths" // https://golang.org/issues/26904 replace github.com/dgrijalva/jwt-go v3.2.0+incompatible => github.com/form3tech-oss/jwt-go v0.0.0-20200915135329-9162a5abdbc0 + +// Pin gRPC back to v1.29.1 (the version required by Kubernetes), but also override a module that's only used in some tests. +// This is required because sometime after v1.29.1, they moved this package into a separate module. +replace ( + google.golang.org/grpc => google.golang.org/grpc v1.29.1 + google.golang.org/grpc/examples => ./hack/dependencyhacks/grpcexamples/ +) diff --git a/go.sum b/go.sum index 28caead8..6635ed05 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,4 @@ bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg= @@ -59,6 +58,7 @@ github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMo github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= @@ -78,6 +78,7 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/aws/aws-sdk-go v1.23.19/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= @@ -104,7 +105,7 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= @@ -113,6 +114,7 @@ github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20200107194136-26c1120b8d41/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY= @@ -154,6 +156,8 @@ github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70d github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v17.12.0-ce-rc1.0.20201201034508-7d75c1d40d88+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= @@ -173,8 +177,7 @@ github.com/elazarl/goproxy v0.0.0-20181003060214-f58a169a71a5/go.mod h1:/Zj4wYkg github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -192,6 +195,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8= github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= @@ -210,26 +215,75 @@ github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/stdr v0.4.0 h1:ijk9G/xzDRZdMU1QRhLYdHuWvNZWqte+NZMOGsiKWbc= github.com/go-logr/stdr v0.4.0/go.mod h1:NO1vneyJDqKVgJYnxhwXWWmQPOvNM391IG3H8ql3jiA= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.3/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.0/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= +github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2es0x5/IbjY= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= +github.com/go-openapi/runtime v0.19.26/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.6/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ= github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= +github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= github.com/gobuffalo/attrs v0.1.0/go.mod h1:fmNpaWyHM0tRm8gCZWKx8yY9fvaNLo2PyzBNSrBZ5Hw= github.com/gobuffalo/buffalo v0.12.8-0.20181004233540-fac9bb505aa8/go.mod h1:sLyT7/dceRXJUxSsE813JTQtA3Eb1vjxWfo/N//vXIY= github.com/gobuffalo/buffalo v0.13.0/go.mod h1:Mjn1Ba9wpIbpbrD+lIDMy99pQ0H0LiddMIIDGse7qT4= @@ -249,6 +303,8 @@ github.com/gobuffalo/buffalo-plugins v1.10.0/go.mod h1:4osg8d9s60txLuGwXnqH+RCjP github.com/gobuffalo/buffalo-plugins v1.11.0/go.mod h1:rtIvAYRjYibgmWhnjKmo7OadtnxuMG5ZQLr25ozAzjg= github.com/gobuffalo/buffalo-plugins v1.15.0/go.mod h1:BqSx01nwgKUQr/MArXzFkSD0QvdJidiky1OKgyfgrK8= github.com/gobuffalo/buffalo-pop v1.0.5/go.mod h1:Fw/LfFDnSmB/vvQXPvcXEjzP98Tc+AudyNWUBWKCwQ8= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= github.com/gobuffalo/envy v1.6.4/go.mod h1:Abh+Jfw475/NWtYMEt+hnJWRiC8INKWibIMyNt1w2Mc= github.com/gobuffalo/envy v1.6.5/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ= github.com/gobuffalo/envy v1.6.6/go.mod h1:N+GkhhZ/93bGZc6ZKhJLP6+m+tCNPKwgSpH9kaifseQ= @@ -276,6 +332,7 @@ github.com/gobuffalo/events v1.3.1/go.mod h1:9JOkQVoyRtailYVE/JJ2ZQ/6i4gTjM5t2Hs github.com/gobuffalo/events v1.4.1/go.mod h1:SjXgWKpeSuvQDvGhgMz5IXx3Czu+IbL+XPLR41NvVQY= github.com/gobuffalo/fizz v1.0.12/go.mod h1:C0sltPxpYK8Ftvf64kbsQa2yiCZY4RZviurNxXdAKwc= github.com/gobuffalo/fizz v1.9.8/go.mod h1:w1FEn1yKNVCc49KnADGyYGRPH7jFON3ak4Bj1yUudHo= +github.com/gobuffalo/fizz v1.10.0/go.mod h1:J2XGPO0AfJ1zKw7+2BA+6FEGAkyEsdCOLvN93WCT2WI= github.com/gobuffalo/flect v0.0.0-20180907193754-dc14d8acaf9f/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA= github.com/gobuffalo/flect v0.0.0-20181002182613-4571df4b1daf/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA= github.com/gobuffalo/flect v0.0.0-20181007231023-ae7ed6bfe683/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA= @@ -287,6 +344,9 @@ github.com/gobuffalo/flect v0.0.0-20181114183036-47375f6d8328/go.mod h1:0HvNbHdf github.com/gobuffalo/flect v0.0.0-20181210151238-24a2b68e0316/go.mod h1:en58vff74S9b99Eg42Dr+/9yPu437QjlNsO/hBYPuOk= github.com/gobuffalo/flect v0.0.0-20190104192022-4af577e09bf2/go.mod h1:en58vff74S9b99Eg42Dr+/9yPu437QjlNsO/hBYPuOk= github.com/gobuffalo/flect v0.0.0-20190117212819-a62e61d96794/go.mod h1:397QT6v05LkZkn07oJXXT6y9FCfwC8Pug0WA2/2mE9k= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gobuffalo/flect v0.2.1/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc= @@ -313,6 +373,10 @@ github.com/gobuffalo/genny v0.0.0-20181207164119-84844398a37d/go.mod h1:y0ysCHGG github.com/gobuffalo/genny v0.0.0-20181211165820-e26c8466f14d/go.mod h1:sHnK+ZSU4e2feXP3PA29ouij6PUEiN+RCwECjCTB3yM= github.com/gobuffalo/genny v0.0.0-20190104222617-a71664fc38e7/go.mod h1:QPsQ1FnhEsiU8f+O0qKWXz2RE4TiDqLVChWkBuh1WaY= github.com/gobuffalo/genny v0.0.0-20190112155932-f31a84fcacf5/go.mod h1:CIaHCrSIuJ4il6ka3Hub4DR4adDrGoXGEEt2FbBxoIo= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= github.com/gobuffalo/genny v0.2.0/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= github.com/gobuffalo/genny v0.3.0/go.mod h1:ywJ2CoXrTZj7rbS8HTbzv7uybnLKlsNSBhEQ+yFI3E8= github.com/gobuffalo/genny v0.6.0/go.mod h1:Vigx9VDiNscYpa/LwrURqGXLSIbzTfapt9+K6gF1kTA= @@ -322,6 +386,9 @@ github.com/gobuffalo/github_flavored_markdown v1.0.4/go.mod h1:uRowCdK+q8d/RF0Kt github.com/gobuffalo/github_flavored_markdown v1.0.5/go.mod h1:U0643QShPF+OF2tJvYNiYDLDGDuQmJZXsf/bHOJPsMY= github.com/gobuffalo/github_flavored_markdown v1.0.7/go.mod h1:w93Pd9Lz6LvyQXEG6DktTPHkOtCbr+arAD5mkwMzXLI= github.com/gobuffalo/github_flavored_markdown v1.1.0/go.mod h1:TSpTKWcRTI0+v7W3x8dkSKMLJSUpuVitlptCkpeY8ic= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= github.com/gobuffalo/gogen v0.2.0/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= github.com/gobuffalo/helpers v0.2.2/go.mod h1:xYbzUdCUpVzLwLnqV8HIjT6hmG0Cs7YIBCJkNM597jw= github.com/gobuffalo/helpers v0.2.4/go.mod h1:NX7v27yxPDOPTgUFYmJ5ow37EbxdoLraucOGvMNawyk= @@ -381,6 +448,7 @@ github.com/gobuffalo/packd v0.0.0-20181114190715-f25c5d2471d7/go.mod h1:Yf2toFaI github.com/gobuffalo/packd v0.0.0-20181124090624-311c6248e5fb/go.mod h1:Foenia9ZvITEvG05ab6XpiD5EfBHPL8A6hush8SJ0o8= github.com/gobuffalo/packd v0.0.0-20181207120301-c49825f8f6f4/go.mod h1:LYc0TGKFBBFTRC9dg2pcRcMqGCTMD7T2BIMP7OBuQAA= github.com/gobuffalo/packd v0.0.0-20181212173646-eca3b8fd6687/go.mod h1:LYc0TGKFBBFTRC9dg2pcRcMqGCTMD7T2BIMP7OBuQAA= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= github.com/gobuffalo/packd v0.2.0/go.mod h1:k2CkHP3bjbqL2GwxwhxUy1DgnlbW644hkLC9iIUvZwY= github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= @@ -400,9 +468,12 @@ github.com/gobuffalo/packr/v2 v2.0.0-rc.12/go.mod h1:FV1zZTsVFi1DSCboO36Xgs4pzCZ github.com/gobuffalo/packr/v2 v2.0.0-rc.13/go.mod h1:2Mp7GhBFMdJlOK8vGfl7SYtfMP3+5roE39ejlfjw0rA= github.com/gobuffalo/packr/v2 v2.0.0-rc.14/go.mod h1:06otbrNvDKO1eNQ3b8hst+1010UooI2MFg+B2Ze4MV8= github.com/gobuffalo/packr/v2 v2.0.0-rc.15/go.mod h1:IMe7H2nJvcKXSF90y4X1rjYIRlNMJYCxEhssBXNZwWs= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= github.com/gobuffalo/packr/v2 v2.4.0/go.mod h1:ra341gygw9/61nSjAbfwcwh8IrYL4WmR4IsPkPBhQiY= github.com/gobuffalo/packr/v2 v2.5.2/go.mod h1:sgEE1xNZ6G0FNN5xn9pevVu4nywaxHvgup67xisti08= github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= +github.com/gobuffalo/packr/v2 v2.8.0/go.mod h1:PDk2k3vGevNE3SwVyVRgQCCXETC9SaONCNSXT1Q8M1g= github.com/gobuffalo/plush v3.7.16+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= github.com/gobuffalo/plush v3.7.20+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= github.com/gobuffalo/plush v3.7.21+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= @@ -424,6 +495,7 @@ github.com/gobuffalo/pop v4.8.3+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVD github.com/gobuffalo/pop v4.8.4+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= github.com/gobuffalo/pop v4.13.1+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= github.com/gobuffalo/pop/v5 v5.0.11/go.mod h1:mZJHJbA3cy2V18abXYuVop2ldEJ8UZ2DK6qOekC5u5g= +github.com/gobuffalo/pop/v5 v5.3.1/go.mod h1:vcEDhh6cJ3WVENqJDFt/6z7zNb7lLnlN8vj3n5G9rYA= github.com/gobuffalo/release v1.0.35/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4= github.com/gobuffalo/release v1.0.38/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4= github.com/gobuffalo/release v1.0.42/go.mod h1:RPs7EtafH4oylgetOJpGP0yCZZUiO4vqHfTHJjSdpug= @@ -464,7 +536,6 @@ github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14j github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid/v3 v3.1.2/go.mod h1:xPwMqoocQ1L5G6pXX5BcE7N5jlzn2o19oqAKxwZW/kI= -github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -473,7 +544,6 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/gddo v0.0.0-20180828051604-96d2a289f41e/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -505,6 +575,7 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -568,9 +639,11 @@ github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBt github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -592,6 +665,7 @@ github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inhies/go-bytesize v0.0.0-20201103132853-d0aed0d254f8/go.mod h1:KrtyD5PFj++GKkFS/7/RRrfnRhAMGQwy75GLCHWrCNs= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -601,6 +675,7 @@ github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= github.com/jackc/pgconn v1.3.2/go.mod h1:LvCquS3HbBKwgl7KbX9KyqEIumJAbm1UMcTvGaIf3bM= github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.6.0/go.mod h1:yeseQo4xhQbgyJs2c87RAXOH2i624N0Fh1KSPJya7qo= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= @@ -610,20 +685,24 @@ github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= +github.com/jackc/pgtype v1.3.0/go.mod h1:b0JqxHvPmljG+HQ5IsvQ0yqeSi4nGcDTVjFoiLDb0Ik= github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.4.1/go.mod h1:6iSW+JznC0YT+SgBn7rNxoEBsBgSmnC5FwyCekOGUiE= +github.com/jackc/pgx/v4 v4.6.0/go.mod h1:vPh43ZzxijXUVJ+t/EmXBtFmbFVO72cuneCT9oAlxAg= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jandelgado/gcov2lcov v1.0.4-0.20210120124023-b83752c6dc08/go.mod h1:NnSxK6TMlg1oGDBfGelGbjgorT5/L3cchlbtgFYZSss= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= @@ -649,14 +728,20 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= github.com/karrick/godirwalk v1.7.7/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= github.com/karrick/godirwalk v1.7.8/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/karrick/godirwalk v1.10.9/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/karrick/godirwalk v1.15.3/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/karrick/godirwalk v1.15.5/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/knadh/koanf v0.14.1-0.20201201075439-e0853799f9ec/go.mod h1:H5mEFsTeWizwFXHKtsITL5ipsLTuAMQoGuQpp+1JL9U= github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -679,18 +764,22 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/luna-duclos/instrumentedsql v0.0.0-20181127104832-b7d587d28109/go.mod h1:PWUIzhtavmOR965zfawVsHXbEuU1G29BPZ/CB3C7jXk= github.com/luna-duclos/instrumentedsql v1.1.2/go.mod h1:4LGbEqDnopzNAiyxPPDXhLspyunZxgPTMJBKtC6U0BQ= -github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/luna-duclos/instrumentedsql v1.1.3/go.mod h1:9J1njvFds+zN7y85EDhN9XNQLANWwZt2ULeIC8yMNYs= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/markbates/deplist v1.0.4/go.mod h1:gRRbPbbuA8TmMiRvaOzUlRfzfjeCCBqX2A6arxN01MM= github.com/markbates/deplist v1.0.5/go.mod h1:gRRbPbbuA8TmMiRvaOzUlRfzfjeCCBqX2A6arxN01MM= github.com/markbates/deplist v1.1.3/go.mod h1:BF7ioVzAJYEtzQN/os4rt8H8Ti3h0T7EoN+7eyALktE= +github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= github.com/markbates/going v1.0.2/go.mod h1:UWCk3zm0UKefHZ7l8BNqi26UyiEMniznk8naLdTcy6c= github.com/markbates/grift v1.0.4/go.mod h1:wbmtW74veyx+cgfwFhlnnMWqhoz55rnHR47oMXzsyVs= github.com/markbates/hmax v1.0.0/go.mod h1:cOkR9dktiESxIMu+65oc/r/bdY4bE8zZw3OLhLx0X2c= @@ -750,6 +839,7 @@ github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYG github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2/go.mod h1:TjQg8pa4iejrUrjiz0MCtMV38jdMNW4doKSiBrEvCQQ= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= @@ -760,6 +850,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/monoculum/formam v0.0.0-20180901015400-4e68be1d79ba/go.mod h1:RKgILGEJq24YyJ2ban8EO0RUVSJlF1pGsEvoLEACr/Q= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/moul/http2curl v0.0.0-20170919181001-9ac6cf4d929b/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -803,19 +895,20 @@ github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rm github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.1.1-0.20190913142402-a7454ce5950e/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/ory/analytics-go/v4 v4.0.0/go.mod h1:FMx9cLRD9xN+XevPvZ5FDMfignpmcqPP6FUKnJ9/MmE= github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= github.com/ory/dockertest/v3 v3.5.4/go.mod h1:J8ZUbNB2FOhm1cFZW9xBpDsODqsSWcyYgtJYVPcnF70= +github.com/ory/dockertest/v3 v3.6.3/go.mod h1:EFLcVUOl8qCwp9NyDAcCDtq/QviLtYswW/VbWzUnTNE= github.com/ory/fosite v0.29.0/go.mod h1:0atSZmXO7CAcs6NPMI/Qtot8tmZYj04Nddoold4S2h0= -github.com/ory/fosite v0.39.0 h1:u1Ct/ME7XYzREvufr7ehBIdq/KatjVLIYg/ABqWzprw= -github.com/ory/fosite v0.39.0/go.mod h1:37r59qkOSPueYKmaA7EHiXrDMF1B+XPN+MgkZgTRg3Y= +github.com/ory/fosite v0.40.2 h1:xOS/1kPOlk1LqnAEnNXUqNGdMwvHBqw20ZbURaK+qoM= +github.com/ory/fosite v0.40.2/go.mod h1:KbDZzqSDLaOLDN2haRIsBQHUhl8MQp66cYMqCpO/8Mg= github.com/ory/go-acc v0.0.0-20181118080137-ddc355013f90/go.mod h1:sxnvPCxChFuSmTJGj8FdMupeq1BezCiEpDjTUXQ4hf4= -github.com/ory/go-acc v0.2.5 h1:31irXHzG2vnKQSE4weJm7AdfrnpaVjVCq3nD7viXCJE= -github.com/ory/go-acc v0.2.5/go.mod h1:4Kb/UnPcT8qRAk3IAxta+hvVapdxTLWtrr7bFLlEgpw= +github.com/ory/go-acc v0.2.6 h1:YfI+L9dxI7QCtWn2RbawqO0vXhiThdXu/RgizJBbaq0= +github.com/ory/go-acc v0.2.6/go.mod h1:4Kb/UnPcT8qRAk3IAxta+hvVapdxTLWtrr7bFLlEgpw= github.com/ory/go-convenience v0.1.0 h1:zouLKfF2GoSGnJwGq+PE/nJAE6dj2Zj5QlTgmMTsTS8= github.com/ory/go-convenience v0.1.0/go.mod h1:uEY/a60PL5c12nYz4V5cHY03IBmwIAEm8TWB0yn9KNs= github.com/ory/gojsonreference v0.0.0-20190720135523-6b606c2d8ee8/go.mod h1:wsH1C4nIeeQClDtD5AH7kF1uTS6zWyqfjVDTmB0Em7A= @@ -823,6 +916,7 @@ github.com/ory/gojsonschema v1.1.1-0.20190919112458-f254ca73d5e9/go.mod h1:BNZpd github.com/ory/herodot v0.6.2/go.mod h1:3BOneqcyBsVybCPAJoi92KN2BpJHcmDqAMcAAaJiJow= github.com/ory/herodot v0.7.0/go.mod h1:YXKOfAXYdQojDP5sD8m0ajowq3+QXNdtxA+QiUXBwn0= github.com/ory/herodot v0.8.3/go.mod h1:rvLjxOAlU5omtmgjCfazQX2N82EpMfl3BytBWc1jjsk= +github.com/ory/herodot v0.9.2/go.mod h1:Da2HXR8mpwPbPrH+Gv9qV8mM5gI3v+PoJ69BA4l2RAk= github.com/ory/jsonschema/v3 v3.0.1/go.mod h1:jgLHekkFk0uiGdEWGleC+tOm6JSSP8cbf17PnBuGXlw= github.com/ory/viper v1.5.6/go.mod h1:TYmpFpKLxjQwvT4f0QPpkOn4sDXU1kDgAwJpgLYiQ28= github.com/ory/viper v1.7.4/go.mod h1:T6sodNZKNGPpashUOk7EtXz2isovz8oCd57GNVkkNmE= @@ -831,8 +925,9 @@ github.com/ory/viper v1.7.5/go.mod h1:ypOuyJmEUb3oENywQZRgeAMwqgOyDqwboO1tj3DjTa github.com/ory/x v0.0.84/go.mod h1:RXLPBG7B+hAViONVg0sHwK+U/ie1Y/NeXrq1JcARfoE= github.com/ory/x v0.0.93/go.mod h1:lfcTaGXpTZs7IEQAW00r9EtTCOxD//SiP5uWtNiz31g= github.com/ory/x v0.0.110/go.mod h1:DJfkE3GdakhshNhw4zlKoRaL/ozg/lcTahA9OCih2BE= -github.com/ory/x v0.0.162 h1:xE/UBmmMlInTvlgGXUyo+VeZAcWU5gyWb/xh6jmBWsI= -github.com/ory/x v0.0.162/go.mod h1:sj3z/MeCrAyNFFTfN6yK1nTmHXGSFnw+QwIIQ/Rowec= +github.com/ory/x v0.0.127/go.mod h1:FwUujfFuCj5d+xgLn4fGMYPnzriR5bdAIulFXMtnK0M= +github.com/ory/x v0.0.212 h1:7PpEOdrXGP/tMGyv0m1ERy74oCS/Z11hlkxVs9OC1GE= +github.com/ory/x v0.0.212/go.mod h1:RDxYOolvMdzumYnHWha8D+RoLjYtGszyDDed4OCGC54= github.com/parnurzeal/gorequest v0.2.15/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= @@ -840,9 +935,11 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI= @@ -885,6 +982,7 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rhnvrm/simples3 v0.5.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.0.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -911,6 +1009,7 @@ github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZ github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210219220335-367fa274be2c/go.mod h1:/THDZYi7F/BsVEcYzYPqdcWFQ+1C2InkawTKfLOAnzg= github.com/segmentio/analytics-go v3.0.1+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48= github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48= github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c/go.mod h1:kJ9mm9YmoWSkk+oQ+5Cj8DEoRCX2JT6As4kEtIIOp1M= @@ -936,6 +1035,7 @@ github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8 github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= @@ -960,11 +1060,13 @@ github.com/spf13/afero v1.3.2 h1:GDarE4TJQI52kYSbSAmLiId1Elfj+xgSDqrUZxFhxlU= github.com/spf13/afero v1.3.2/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.2-0.20200723214538-8d17101741c8 h1:GbJaXkBXPYlxE45H4g2wo0Hb4TGzv/YbHVA1OGqx+mo= +github.com/spf13/cast v1.3.2-0.20200723214538-8d17101741c8/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= @@ -986,6 +1088,7 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A= +github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693/go.mod h1:6hSY48PjDm4UObWmGLyJE9DxYVKTgR9kbCspXXJEhcU= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1001,9 +1104,15 @@ github.com/subosito/gotenv v1.1.1/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/gjson v1.6.8/go.mod h1:zeFuBCIqD4sN/gmqBzZ4j7Jd6UcA2Fc56x7QFsv+8fI= +github.com/tidwall/gjson v1.7.1/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/sjson v1.0.4/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y= +github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE= github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= @@ -1019,13 +1128,14 @@ github.com/unrolled/secure v0.0.0-20180918153822-f340ee86eb8b/go.mod h1:mnPT77IA github.com/unrolled/secure v0.0.0-20181005190816-ff9db2ff917f/go.mod h1:mnPT77IAdsi/kV7+Es7y+pXALeV3h7G6dQF6mNYjcLA= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= @@ -1039,6 +1149,10 @@ go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 h1:1JFLBqwIgdyHN1ZtgjTBwO+blA6gVOmZurpiMEsETKo= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= +go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA= @@ -1073,11 +1187,14 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190102171810-8d7daa0c54b3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -1112,7 +1229,6 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -1135,7 +1251,6 @@ golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJt golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180921000356-2f5d2388922f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1152,6 +1267,7 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -1174,7 +1290,7 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -1192,16 +1308,15 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1225,12 +1340,15 @@ golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1261,7 +1379,6 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1299,6 +1416,7 @@ golang.org/x/tools v0.0.0-20181013182035-5e66757b835f/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20181017214349-06f26fdaaa28/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181024171208-a2dc47679d30/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181026183834-f60e5f99f081/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181105230042-78dc5bac0cac/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181107215632-34b416bd17b3/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181114190951-94339b83286c/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1313,7 +1431,6 @@ golang.org/x/tools v0.0.0-20181212172921-837e80568c09/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190102213336-ca9055ed7d04/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190104182027-498d95493402/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190111214448-fc1d57b08d7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190118193359-16909d206f00/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -1321,13 +1438,18 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190613204242-ed0dc450797f/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= @@ -1358,10 +1480,10 @@ golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200721223218-6123e77877b2/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= @@ -1388,7 +1510,6 @@ google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1416,20 +1537,11 @@ google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1487,6 +1599,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= @@ -1516,8 +1629,9 @@ k8s.io/gengo v0.0.0-20210203185629-de9496dff47b h1:bAU8IlrMA6KbP0dIg/sVSJn95pDCU k8s.io/gengo v0.0.0-20210203185629-de9496dff47b/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.8.0 h1:Q3gmuM9hKEjefWFFYF0Mat+YyFJvsUyYuwyNNJ5C9Ts= k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= +k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/kube-aggregator v0.21.1 h1:3pPRhOXZcJYjNDjPDizFx0G5//DArWKANZE03J5z8Ck= k8s.io/kube-aggregator v0.21.1/go.mod h1:cAZ0n02IiSl57sQSHz4vvrz3upQRMbytOiZnpPJaQzQ= k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7 h1:vEx13qjvaZ4yfObSSXW7BrMc/KQBBT/Jyee8XtLf4x0= diff --git a/hack/dependencyhacks/grpcexamples/go.mod b/hack/dependencyhacks/grpcexamples/go.mod new file mode 100644 index 00000000..47491cc5 --- /dev/null +++ b/hack/dependencyhacks/grpcexamples/go.mod @@ -0,0 +1,3 @@ +module google.golang.org/grpc/examples + +go 1.14 diff --git a/hack/module.sh b/hack/module.sh index bc63298f..f7c2d0d2 100755 --- a/hack/module.sh +++ b/hack/module.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Copyright 2020 the Pinniped contributors. All Rights Reserved. +# Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 set -euo pipefail @@ -46,7 +46,7 @@ function with_modules() { env_vars="KUBE_CACHE_MUTATION_DETECTOR=${kube_cache_mutation_detector} KUBE_PANIC_WATCH_DECODE_ERROR=${kube_panic_watch_decode_error}" pushd "${ROOT}" >/dev/null - for mod_file in $(find . -maxdepth 4 -not -path "./generated/*" -name go.mod | sort); do + for mod_file in $(find . -maxdepth 4 -not -path "./generated/*" -not -path "./hack/*" -name go.mod | sort); do mod_dir="$(dirname "${mod_file}")" ( echo "=> " diff --git a/internal/concierge/impersonator/config.go b/internal/concierge/impersonator/config.go deleted file mode 100644 index 60ab59bd..00000000 --- a/internal/concierge/impersonator/config.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2021 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package impersonator - -import ( - "fmt" - - v1 "k8s.io/api/core/v1" - "sigs.k8s.io/yaml" -) - -type Mode string - -const ( - // Explicitly enable the impersonation proxy. - ModeEnabled Mode = "enabled" - - // Explicitly disable the impersonation proxy. - ModeDisabled Mode = "disabled" - - // Allow the proxy to decide if it should be enabled or disabled based upon the cluster in which it is running. - ModeAuto Mode = "auto" -) - -const ( - ConfigMapDataKey = "config.yaml" -) - -type Config struct { - // Enable or disable the impersonation proxy. Optional. Defaults to ModeAuto. - Mode Mode `json:"mode,omitempty"` - - // Used when creating TLS certificates and for clients to discover the endpoint. Optional. When not specified, if the - // impersonation proxy is started, then it will automatically create a LoadBalancer Service and use its ingress as the - // endpoint. - // - // When specified, it may be a hostname or IP address, optionally with a port number, of the impersonation proxy - // for clients to use from outside the cluster. E.g. myhost.mycompany.com:8443. Clients should assume that they should - // connect via HTTPS to this service. - Endpoint string `json:"endpoint,omitempty"` -} - -func (c *Config) HasEndpoint() bool { - return c.Endpoint != "" -} - -func NewConfig() *Config { - return &Config{Mode: ModeAuto} -} - -func ConfigFromConfigMap(configMap *v1.ConfigMap) (*Config, error) { - stringConfig, ok := configMap.Data[ConfigMapDataKey] - if !ok { - return nil, fmt.Errorf(`ConfigMap is missing expected key "%s"`, ConfigMapDataKey) - } - config := NewConfig() - if err := yaml.Unmarshal([]byte(stringConfig), config); err != nil { - return nil, fmt.Errorf("decode yaml: %w", err) - } - if config.Mode != ModeAuto && config.Mode != ModeEnabled && config.Mode != ModeDisabled { - return nil, fmt.Errorf(`illegal value for "mode": %s`, config.Mode) - } - return config, nil -} diff --git a/internal/concierge/impersonator/config_test.go b/internal/concierge/impersonator/config_test.go deleted file mode 100644 index b734dba9..00000000 --- a/internal/concierge/impersonator/config_test.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2021 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package impersonator - -import ( - "testing" - - "github.com/stretchr/testify/require" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "go.pinniped.dev/internal/here" -) - -func TestNewConfig(t *testing.T) { - // It defaults the mode. - require.Equal(t, &Config{Mode: ModeAuto}, NewConfig()) -} - -func TestHasEndpoint(t *testing.T) { - configWithoutEndpoint := Config{} - configWithEndpoint := Config{Endpoint: "something"} - require.False(t, configWithoutEndpoint.HasEndpoint()) - require.True(t, configWithEndpoint.HasEndpoint()) -} - -func TestConfigFromConfigMap(t *testing.T) { - tests := []struct { - name string - configMap *v1.ConfigMap - wantConfig *Config - wantError string - }{ - { - name: "fully configured, valid config", - configMap: &v1.ConfigMap{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{}, - Data: map[string]string{ - "config.yaml": here.Doc(` - mode: enabled - endpoint: proxy.example.com:8443 - `), - }, - }, - wantConfig: &Config{ - Mode: "enabled", - Endpoint: "proxy.example.com:8443", - }, - }, - { - name: "empty, valid config", - configMap: &v1.ConfigMap{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{}, - Data: map[string]string{ - "config.yaml": "", - }, - }, - wantConfig: &Config{ - Mode: "auto", - Endpoint: "", - }, - }, - { - name: "valid config with mode enabled", - configMap: &v1.ConfigMap{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{}, - Data: map[string]string{ - "config.yaml": "mode: enabled", - }, - }, - wantConfig: &Config{ - Mode: "enabled", - Endpoint: "", - }, - }, - { - name: "valid config with mode disabled", - configMap: &v1.ConfigMap{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{}, - Data: map[string]string{ - "config.yaml": "mode: disabled", - }, - }, - wantConfig: &Config{ - Mode: "disabled", - Endpoint: "", - }, - }, - { - name: "valid config with mode auto", - configMap: &v1.ConfigMap{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{}, - Data: map[string]string{ - "config.yaml": "mode: auto", - }, - }, - wantConfig: &Config{ - Mode: "auto", - Endpoint: "", - }, - }, - { - name: "wrong key in configmap", - configMap: &v1.ConfigMap{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{}, - Data: map[string]string{ - "wrong-key": "", - }, - }, - wantError: `ConfigMap is missing expected key "config.yaml"`, - }, - { - name: "illegal yaml in configmap", - configMap: &v1.ConfigMap{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{}, - Data: map[string]string{ - "config.yaml": "this is not yaml", - }, - }, - wantError: "decode yaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type impersonator.Config", - }, - { - name: "illegal value for mode in configmap", - configMap: &v1.ConfigMap{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{}, - Data: map[string]string{ - "config.yaml": "mode: unexpected-value", - }, - }, - wantError: `illegal value for "mode": unexpected-value`, - }, - } - - for _, tt := range tests { - test := tt - t.Run(test.name, func(t *testing.T) { - config, err := ConfigFromConfigMap(test.configMap) - require.Equal(t, test.wantConfig, config) - if test.wantError != "" { - require.EqualError(t, err, test.wantError) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/internal/concierge/impersonator/impersonator.go b/internal/concierge/impersonator/impersonator.go index 5778b795..2eeaf25d 100644 --- a/internal/concierge/impersonator/impersonator.go +++ b/internal/concierge/impersonator/impersonator.go @@ -195,7 +195,6 @@ func newInternal( //nolint:funlen // yeah, it's kind of long. } // wire up a fake audit backend at the metadata level so we can preserve the original user during nested impersonation - // TODO: wire up the real std out logging audit backend based on plog log level serverConfig.AuditPolicyChecker = policy.FakeChecker(auditinternal.LevelMetadata, nil) serverConfig.AuditBackend = &auditfake.Backend{} diff --git a/internal/config/concierge/config.go b/internal/config/concierge/config.go index ad6a00df..cbe9d7f9 100644 --- a/internal/config/concierge/config.go +++ b/internal/config/concierge/config.go @@ -108,12 +108,12 @@ func validateNames(names *NamesConfigSpec) error { if names.APIService == "" { missingNames = append(missingNames, "apiService") } - if names.ImpersonationConfigMap == "" { - missingNames = append(missingNames, "impersonationConfigMap") - } if names.ImpersonationLoadBalancerService == "" { missingNames = append(missingNames, "impersonationLoadBalancerService") } + if names.ImpersonationClusterIPService == "" { + missingNames = append(missingNames, "impersonationClusterIPService") + } if names.ImpersonationTLSCertificateSecret == "" { missingNames = append(missingNames, "impersonationTLSCertificateSecret") } diff --git a/internal/config/concierge/config_test.go b/internal/config/concierge/config_test.go index baebf8c3..c29fb5f6 100644 --- a/internal/config/concierge/config_test.go +++ b/internal/config/concierge/config_test.go @@ -38,13 +38,14 @@ func TestFromPath(t *testing.T) { credentialIssuer: pinniped-config apiService: pinniped-api kubeCertAgentPrefix: kube-cert-agent-prefix - impersonationConfigMap: impersonationConfigMap-value impersonationLoadBalancerService: impersonationLoadBalancerService-value + impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value + extraName: extraName-value labels: myLabelKey1: myLabelValue1 myLabelKey2: myLabelValue2 @@ -69,8 +70,8 @@ func TestFromPath(t *testing.T) { ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate", CredentialIssuer: "pinniped-config", APIService: "pinniped-api", - ImpersonationConfigMap: "impersonationConfigMap-value", ImpersonationLoadBalancerService: "impersonationLoadBalancerService-value", + ImpersonationClusterIPService: "impersonationClusterIPService-value", ImpersonationTLSCertificateSecret: "impersonationTLSCertificateSecret-value", ImpersonationCACertificateSecret: "impersonationCACertificateSecret-value", ImpersonationSignerSecret: "impersonationSignerSecret-value", @@ -96,8 +97,8 @@ func TestFromPath(t *testing.T) { servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api - impersonationConfigMap: impersonationConfigMap-value impersonationLoadBalancerService: impersonationLoadBalancerService-value + impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value @@ -118,8 +119,8 @@ func TestFromPath(t *testing.T) { ServingCertificateSecret: "pinniped-concierge-api-tls-serving-certificate", CredentialIssuer: "pinniped-config", APIService: "pinniped-api", - ImpersonationConfigMap: "impersonationConfigMap-value", ImpersonationLoadBalancerService: "impersonationLoadBalancerService-value", + ImpersonationClusterIPService: "impersonationClusterIPService-value", ImpersonationTLSCertificateSecret: "impersonationTLSCertificateSecret-value", ImpersonationCACertificateSecret: "impersonationCACertificateSecret-value", ImpersonationSignerSecret: "impersonationSignerSecret-value", @@ -136,8 +137,8 @@ func TestFromPath(t *testing.T) { name: "Empty", yaml: here.Doc(``), wantError: "validate names: missing required names: servingCertificateSecret, credentialIssuer, " + - "apiService, impersonationConfigMap, impersonationLoadBalancerService, " + - "impersonationTLSCertificateSecret, impersonationCACertificateSecret, " + + "apiService, impersonationLoadBalancerService, " + + "impersonationClusterIPService, impersonationTLSCertificateSecret, impersonationCACertificateSecret, " + "impersonationSignerSecret, agentServiceAccount", }, { @@ -147,8 +148,8 @@ func TestFromPath(t *testing.T) { names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config - impersonationConfigMap: impersonationConfigMap-value impersonationLoadBalancerService: impersonationLoadBalancerService-value + impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value @@ -163,8 +164,8 @@ func TestFromPath(t *testing.T) { names: servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate apiService: pinniped-api - impersonationConfigMap: impersonationConfigMap-value impersonationLoadBalancerService: impersonationLoadBalancerService-value + impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value @@ -179,8 +180,8 @@ func TestFromPath(t *testing.T) { names: credentialIssuer: pinniped-config apiService: pinniped-api - impersonationConfigMap: impersonationConfigMap-value impersonationLoadBalancerService: impersonationLoadBalancerService-value + impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value @@ -188,22 +189,6 @@ func TestFromPath(t *testing.T) { `), wantError: "validate names: missing required names: servingCertificateSecret", }, - { - name: "Missing impersonationConfigMap name", - yaml: here.Doc(` - --- - names: - servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate - credentialIssuer: pinniped-config - apiService: pinniped-api - impersonationLoadBalancerService: impersonationLoadBalancerService-value - impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value - impersonationCACertificateSecret: impersonationCACertificateSecret-value - impersonationSignerSecret: impersonationSignerSecret-value - agentServiceAccount: agentServiceAccount-value - `), - wantError: "validate names: missing required names: impersonationConfigMap", - }, { name: "Missing impersonationLoadBalancerService name", yaml: here.Doc(` @@ -212,7 +197,7 @@ func TestFromPath(t *testing.T) { servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api - impersonationConfigMap: impersonationConfigMap-value + impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value @@ -220,6 +205,22 @@ func TestFromPath(t *testing.T) { `), wantError: "validate names: missing required names: impersonationLoadBalancerService", }, + { + name: "Missing impersonationClusterIPService name", + yaml: here.Doc(` + --- + names: + servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate + credentialIssuer: pinniped-config + apiService: pinniped-api + impersonationLoadBalancerService: impersonationLoadBalancerService-value + impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value + impersonationCACertificateSecret: impersonationCACertificateSecret-value + impersonationSignerSecret: impersonationSignerSecret-value + agentServiceAccount: agentServiceAccount-value + `), + wantError: "validate names: missing required names: impersonationClusterIPService", + }, { name: "Missing impersonationTLSCertificateSecret name", yaml: here.Doc(` @@ -228,8 +229,8 @@ func TestFromPath(t *testing.T) { servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api - impersonationConfigMap: impersonationConfigMap-value impersonationLoadBalancerService: impersonationLoadBalancerService-value + impersonationClusterIPService: impersonationClusterIPService-value impersonationCACertificateSecret: impersonationCACertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value @@ -244,8 +245,8 @@ func TestFromPath(t *testing.T) { servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api - impersonationConfigMap: impersonationConfigMap-value impersonationLoadBalancerService: impersonationLoadBalancerService-value + impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value @@ -260,8 +261,8 @@ func TestFromPath(t *testing.T) { servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api - impersonationConfigMap: impersonationConfigMap-value impersonationLoadBalancerService: impersonationLoadBalancerService-value + impersonationClusterIPService: impersonationClusterIPService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value agentServiceAccount: agentServiceAccount-value @@ -277,10 +278,11 @@ func TestFromPath(t *testing.T) { credentialIssuer: pinniped-config apiService: pinniped-api impersonationLoadBalancerService: impersonationLoadBalancerService-value + impersonationClusterIPService: impersonationClusterIPService-value impersonationSignerSecret: impersonationSignerSecret-value agentServiceAccount: agentServiceAccount-value `), - wantError: "validate names: missing required names: impersonationConfigMap, " + + wantError: "validate names: missing required names: " + "impersonationTLSCertificateSecret, impersonationCACertificateSecret", }, { @@ -295,7 +297,6 @@ func TestFromPath(t *testing.T) { servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api - impersonationConfigMap: impersonationConfigMap-value impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value @@ -315,7 +316,6 @@ func TestFromPath(t *testing.T) { servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api - impersonationConfigMap: impersonationConfigMap-value impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value @@ -335,7 +335,6 @@ func TestFromPath(t *testing.T) { servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api - impersonationConfigMap: impersonationConfigMap-value impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value @@ -356,7 +355,6 @@ func TestFromPath(t *testing.T) { servingCertificateSecret: pinniped-concierge-api-tls-serving-certificate credentialIssuer: pinniped-config apiService: pinniped-api - impersonationConfigMap: impersonationConfigMap-value impersonationLoadBalancerService: impersonationLoadBalancerService-value impersonationTLSCertificateSecret: impersonationTLSCertificateSecret-value impersonationCACertificateSecret: impersonationCACertificateSecret-value diff --git a/internal/config/concierge/types.go b/internal/config/concierge/types.go index ecd36d0a..6aa6733a 100644 --- a/internal/config/concierge/types.go +++ b/internal/config/concierge/types.go @@ -36,8 +36,8 @@ type NamesConfigSpec struct { ServingCertificateSecret string `json:"servingCertificateSecret"` CredentialIssuer string `json:"credentialIssuer"` APIService string `json:"apiService"` - ImpersonationConfigMap string `json:"impersonationConfigMap"` ImpersonationLoadBalancerService string `json:"impersonationLoadBalancerService"` + ImpersonationClusterIPService string `json:"impersonationClusterIPService"` ImpersonationTLSCertificateSecret string `json:"impersonationTLSCertificateSecret"` ImpersonationCACertificateSecret string `json:"impersonationCACertificateSecret"` ImpersonationSignerSecret string `json:"impersonationSignerSecret"` diff --git a/internal/controller/impersonatorconfig/impersonator_config.go b/internal/controller/impersonatorconfig/impersonator_config.go index c94aae15..29e7dfa7 100644 --- a/internal/controller/impersonatorconfig/impersonator_config.go +++ b/internal/controller/impersonatorconfig/impersonator_config.go @@ -14,19 +14,25 @@ import ( "strings" "time" + "github.com/go-logr/logr" v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/clock" "k8s.io/apimachinery/pkg/util/errors" + utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/intstr" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation" corev1informers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" + conciergeconfiginformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions/config/v1alpha1" "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/clusterhost" "go.pinniped.dev/internal/concierge/impersonator" @@ -36,7 +42,7 @@ import ( "go.pinniped.dev/internal/controller/issuerconfig" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/dynamiccert" - "go.pinniped.dev/internal/plog" + "go.pinniped.dev/internal/endpointaddr" ) const ( @@ -51,9 +57,9 @@ const ( type impersonatorConfigController struct { namespace string - configMapResourceName string credentialIssuerResourceName string generatedLoadBalancerServiceName string + generatedClusterIPServiceName string tlsSecretName string caSecretName string impersonationSignerSecretName string @@ -61,7 +67,7 @@ type impersonatorConfigController struct { k8sClient kubernetes.Interface pinnipedAPIClient pinnipedclientset.Interface - configMapsInformer corev1informers.ConfigMapInformer + credIssuerInformer conciergeconfiginformers.CredentialIssuerInformer servicesInformer corev1informers.ServiceInformer secretsInformer corev1informers.SecretInformer @@ -74,20 +80,21 @@ type impersonatorConfigController struct { serverStopCh chan struct{} errorCh chan error tlsServingCertDynamicCertProvider dynamiccert.Private + infoLog logr.Logger + debugLog logr.Logger } func NewImpersonatorConfigController( namespace string, - configMapResourceName string, credentialIssuerResourceName string, k8sClient kubernetes.Interface, pinnipedAPIClient pinnipedclientset.Interface, - configMapsInformer corev1informers.ConfigMapInformer, + credentialIssuerInformer conciergeconfiginformers.CredentialIssuerInformer, servicesInformer corev1informers.ServiceInformer, secretsInformer corev1informers.SecretInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, - withInitialEvent pinnipedcontroller.WithInitialEventOptionFunc, generatedLoadBalancerServiceName string, + generatedClusterIPServiceName string, tlsSecretName string, caSecretName string, labels map[string]string, @@ -95,22 +102,24 @@ func NewImpersonatorConfigController( impersonatorFunc impersonator.FactoryFunc, impersonationSignerSecretName string, impersonationSigningCertProvider dynamiccert.Provider, + log logr.Logger, ) controllerlib.Controller { secretNames := sets.NewString(tlsSecretName, caSecretName, impersonationSignerSecretName) + log = log.WithName("impersonator-config-controller") return controllerlib.New( controllerlib.Config{ Name: "impersonator-config-controller", Syncer: &impersonatorConfigController{ namespace: namespace, - configMapResourceName: configMapResourceName, credentialIssuerResourceName: credentialIssuerResourceName, generatedLoadBalancerServiceName: generatedLoadBalancerServiceName, + generatedClusterIPServiceName: generatedClusterIPServiceName, tlsSecretName: tlsSecretName, caSecretName: caSecretName, impersonationSignerSecretName: impersonationSignerSecretName, k8sClient: k8sClient, pinnipedAPIClient: pinnipedAPIClient, - configMapsInformer: configMapsInformer, + credIssuerInformer: credentialIssuerInformer, servicesInformer: servicesInformer, secretsInformer: secretsInformer, labels: labels, @@ -118,45 +127,48 @@ func NewImpersonatorConfigController( impersonationSigningCertProvider: impersonationSigningCertProvider, impersonatorFunc: impersonatorFunc, tlsServingCertDynamicCertProvider: dynamiccert.NewServingCert("impersonation-proxy-serving-cert"), + infoLog: log.V(2), + debugLog: log.V(4), }, }, - withInformer( - configMapsInformer, - pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(configMapResourceName, namespace), + withInformer(credentialIssuerInformer, + pinnipedcontroller.SimpleFilterWithSingletonQueue(func(obj metav1.Object) bool { + return obj.GetName() == credentialIssuerResourceName + }), controllerlib.InformerOption{}, ), withInformer( servicesInformer, - pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(generatedLoadBalancerServiceName, namespace), + pinnipedcontroller.SimpleFilterWithSingletonQueue(func(obj metav1.Object) bool { + return obj.GetNamespace() == namespace && obj.GetName() == generatedLoadBalancerServiceName + }), controllerlib.InformerOption{}, ), withInformer( secretsInformer, - pinnipedcontroller.SimpleFilter(func(obj metav1.Object) bool { + pinnipedcontroller.SimpleFilterWithSingletonQueue(func(obj metav1.Object) bool { return obj.GetNamespace() == namespace && secretNames.Has(obj.GetName()) - }, nil), + }), controllerlib.InformerOption{}, ), - // Be sure to run once even if the ConfigMap that the informer is watching doesn't exist so we can implement - // the default configuration behavior. - withInitialEvent(controllerlib.Key{ - Namespace: namespace, - Name: configMapResourceName, - }), - // TODO fix these controller options to make this a singleton queue ) } func (c *impersonatorConfigController) Sync(syncCtx controllerlib.Context) error { - plog.Debug("Starting impersonatorConfigController Sync") + c.debugLog.Info("starting impersonatorConfigController Sync") - strategy, err := c.doSync(syncCtx) + // Load the CredentialIssuer that we'll update with status. + credIssuer, err := c.credIssuerInformer.Lister().Get(c.credentialIssuerResourceName) + if err != nil { + return fmt.Errorf("could not get CredentialIssuer to update: %w", err) + } + strategy, err := c.doSync(syncCtx, credIssuer) if err != nil { strategy = &v1alpha1.CredentialIssuerStrategy{ Type: v1alpha1.ImpersonationProxyStrategyType, Status: v1alpha1.ErrorStrategyStatus, - Reason: v1alpha1.ErrorDuringSetupStrategyReason, + Reason: strategyReasonForError(err), Message: err.Error(), LastUpdateTime: metav1.NewTime(c.clock.Now()), } @@ -164,20 +176,31 @@ func (c *impersonatorConfigController) Sync(syncCtx controllerlib.Context) error c.clearSignerCA() } - updateStrategyErr := c.updateStrategy(syncCtx.Context, strategy) - if updateStrategyErr != nil { - plog.Error("error while updating the CredentialIssuer status", err) - if err == nil { - err = updateStrategyErr - } - } + err = utilerrors.NewAggregate([]error{err, issuerconfig.Update( + syncCtx.Context, + c.pinnipedAPIClient, + credIssuer, + *strategy, + )}) if err == nil { - plog.Debug("Successfully finished impersonatorConfigController Sync") + c.debugLog.Info("successfully finished impersonatorConfigController Sync") } return err } +// strategyReasonForError returns the proper v1alpha1.StrategyReason for a sync error. Some errors are occasionally +// expected because there are multiple pods running, in these cases we should report a Pending reason and we'll +// recover on a following sync. +func strategyReasonForError(err error) v1alpha1.StrategyReason { + switch { + case k8serrors.IsConflict(err), k8serrors.IsAlreadyExists(err): + return v1alpha1.PendingStrategyReason + default: + return v1alpha1.ErrorDuringSetupStrategyReason + } +} + type certNameInfo struct { // ready will be true when the certificate name information is known. // ready will be false when it is pending because we are waiting for a load balancer to get assigned an ip/hostname. @@ -186,7 +209,7 @@ type certNameInfo struct { // The IP address or hostname which was selected to be used as the name in the cert. // Either selectedIP or selectedHostname will be set, but not both. - selectedIP net.IP + selectedIPs []net.IP selectedHostname string // The name of the endpoint to which a client should connect to talk to the impersonator. @@ -194,10 +217,10 @@ type certNameInfo struct { clientEndpoint string } -func (c *impersonatorConfigController) doSync(syncCtx controllerlib.Context) (*v1alpha1.CredentialIssuerStrategy, error) { +func (c *impersonatorConfigController) doSync(syncCtx controllerlib.Context, credIssuer *v1alpha1.CredentialIssuer) (*v1alpha1.CredentialIssuerStrategy, error) { ctx := syncCtx.Context - config, err := c.loadImpersonationProxyConfiguration() + impersonationSpec, err := c.loadImpersonationProxyConfiguration(credIssuer) if err != nil { return nil, err } @@ -212,21 +235,21 @@ func (c *impersonatorConfigController) doSync(syncCtx controllerlib.Context) (*v return nil, err } c.hasControlPlaneNodes = &hasControlPlaneNodes - plog.Debug("Queried for control plane nodes", "foundControlPlaneNodes", hasControlPlaneNodes) + c.debugLog.Info("queried for control plane nodes", "foundControlPlaneNodes", hasControlPlaneNodes) } - if c.shouldHaveImpersonator(config) { + if c.shouldHaveImpersonator(impersonationSpec) { if err = c.ensureImpersonatorIsStarted(syncCtx); err != nil { return nil, err } } else { if err = c.ensureImpersonatorIsStopped(true); err != nil { - return nil, err // TODO write unit test that errors during stopping the server are returned by sync + return nil, err } } - if c.shouldHaveLoadBalancer(config) { - if err = c.ensureLoadBalancerIsStarted(ctx); err != nil { + if c.shouldHaveLoadBalancer(impersonationSpec) { + if err = c.ensureLoadBalancerIsStarted(ctx, impersonationSpec); err != nil { return nil, err } } else { @@ -235,7 +258,17 @@ func (c *impersonatorConfigController) doSync(syncCtx controllerlib.Context) (*v } } - nameInfo, err := c.findDesiredTLSCertificateName(config) + if c.shouldHaveClusterIPService(impersonationSpec) { + if err = c.ensureClusterIPServiceIsStarted(ctx, impersonationSpec); err != nil { + return nil, err + } + } else { + if err = c.ensureClusterIPServiceIsStopped(ctx); err != nil { + return nil, err + } + } + + nameInfo, err := c.findDesiredTLSCertificateName(impersonationSpec) if err != nil { // Unexpected error while determining the name that should go into the certs, so clear any existing certs. c.tlsServingCertDynamicCertProvider.UnsetCertKeyContent() @@ -243,7 +276,7 @@ func (c *impersonatorConfigController) doSync(syncCtx controllerlib.Context) (*v } var impersonationCA *certauthority.CA - if c.shouldHaveTLSSecret(config) { + if c.shouldHaveTLSSecret(impersonationSpec) { if impersonationCA, err = c.ensureCASecretIsCreated(ctx); err != nil { return nil, err } @@ -254,7 +287,7 @@ func (c *impersonatorConfigController) doSync(syncCtx controllerlib.Context) (*v return nil, err } - credentialIssuerStrategyResult := c.doSyncResult(nameInfo, config, impersonationCA) + credentialIssuerStrategyResult := c.doSyncResult(nameInfo, impersonationSpec, impersonationCA) if err = c.loadSignerCA(credentialIssuerStrategyResult.Status); err != nil { return nil, err @@ -263,64 +296,55 @@ func (c *impersonatorConfigController) doSync(syncCtx controllerlib.Context) (*v return credentialIssuerStrategyResult, nil } -func (c *impersonatorConfigController) loadImpersonationProxyConfiguration() (*impersonator.Config, error) { - configMap, err := c.configMapsInformer.Lister().ConfigMaps(c.namespace).Get(c.configMapResourceName) - notFound := k8serrors.IsNotFound(err) - if err != nil && !notFound { - return nil, fmt.Errorf("failed to get %s/%s configmap: %w", c.namespace, c.configMapResourceName, err) +func (c *impersonatorConfigController) loadImpersonationProxyConfiguration(credIssuer *v1alpha1.CredentialIssuer) (*v1alpha1.ImpersonationProxySpec, error) { + // Make a copy of the spec since we got this object from informer cache. + spec := credIssuer.Spec.DeepCopy().ImpersonationProxy + if spec == nil { + return nil, fmt.Errorf("could not load CredentialIssuer: spec.impersonationProxy is nil") } - var config *impersonator.Config - if notFound { - plog.Info("Did not find impersonation proxy config: using default config values", - "configmap", c.configMapResourceName, - "namespace", c.namespace, - ) - config = impersonator.NewConfig() // use default configuration options - } else { - config, err = impersonator.ConfigFromConfigMap(configMap) - if err != nil { - return nil, fmt.Errorf("invalid impersonator configuration: %v", err) - } - plog.Info("Read impersonation proxy config", - "configmap", c.configMapResourceName, - "namespace", c.namespace, - ) + // Default service type to LoadBalancer (this is normally already done via CRD defaulting). + if spec.Service.Type == "" { + spec.Service.Type = v1alpha1.ImpersonationProxyServiceTypeLoadBalancer } - return config, nil + if err := validateCredentialIssuerSpec(spec); err != nil { + return nil, fmt.Errorf("could not load CredentialIssuer spec.impersonationProxy: %w", err) + } + c.debugLog.Info("read impersonation proxy config", "credentialIssuer", c.credentialIssuerResourceName) + return spec, nil } -func (c *impersonatorConfigController) shouldHaveImpersonator(config *impersonator.Config) bool { - return c.enabledByAutoMode(config) || config.Mode == impersonator.ModeEnabled +func (c *impersonatorConfigController) shouldHaveImpersonator(config *v1alpha1.ImpersonationProxySpec) bool { + return c.enabledByAutoMode(config) || config.Mode == v1alpha1.ImpersonationProxyModeEnabled } -func (c *impersonatorConfigController) enabledByAutoMode(config *impersonator.Config) bool { - return config.Mode == impersonator.ModeAuto && !*c.hasControlPlaneNodes +func (c *impersonatorConfigController) enabledByAutoMode(config *v1alpha1.ImpersonationProxySpec) bool { + return config.Mode == v1alpha1.ImpersonationProxyModeAuto && !*c.hasControlPlaneNodes } -func (c *impersonatorConfigController) disabledByAutoMode(config *impersonator.Config) bool { - return config.Mode == impersonator.ModeAuto && *c.hasControlPlaneNodes +func (c *impersonatorConfigController) disabledByAutoMode(config *v1alpha1.ImpersonationProxySpec) bool { + return config.Mode == v1alpha1.ImpersonationProxyModeAuto && *c.hasControlPlaneNodes } -func (c *impersonatorConfigController) disabledExplicitly(config *impersonator.Config) bool { - return config.Mode == impersonator.ModeDisabled +func (c *impersonatorConfigController) disabledExplicitly(config *v1alpha1.ImpersonationProxySpec) bool { + return config.Mode == v1alpha1.ImpersonationProxyModeDisabled } -func (c *impersonatorConfigController) shouldHaveLoadBalancer(config *impersonator.Config) bool { - return c.shouldHaveImpersonator(config) && !config.HasEndpoint() +func (c *impersonatorConfigController) shouldHaveLoadBalancer(config *v1alpha1.ImpersonationProxySpec) bool { + return c.shouldHaveImpersonator(config) && config.Service.Type == v1alpha1.ImpersonationProxyServiceTypeLoadBalancer } -func (c *impersonatorConfigController) shouldHaveTLSSecret(config *impersonator.Config) bool { +func (c *impersonatorConfigController) shouldHaveClusterIPService(config *v1alpha1.ImpersonationProxySpec) bool { + return c.shouldHaveImpersonator(config) && config.Service.Type == v1alpha1.ImpersonationProxyServiceTypeClusterIP +} + +func (c *impersonatorConfigController) shouldHaveTLSSecret(config *v1alpha1.ImpersonationProxySpec) bool { return c.shouldHaveImpersonator(config) } -func (c *impersonatorConfigController) updateStrategy(ctx context.Context, strategy *v1alpha1.CredentialIssuerStrategy) error { - return issuerconfig.UpdateStrategy(ctx, c.credentialIssuerResourceName, c.labels, c.pinnipedAPIClient, *strategy) -} - -func (c *impersonatorConfigController) loadBalancerExists() (bool, error) { - _, err := c.servicesInformer.Lister().Services(c.namespace).Get(c.generatedLoadBalancerServiceName) +func (c *impersonatorConfigController) serviceExists(serviceName string) (bool, error) { + _, err := c.servicesInformer.Lister().Services(c.namespace).Get(serviceName) notFound := k8serrors.IsNotFound(err) if notFound { return false, nil @@ -367,7 +391,7 @@ func (c *impersonatorConfigController) ensureImpersonatorIsStarted(syncCtx contr } } - plog.Info("Starting impersonation proxy", "port", impersonationProxyPort) + c.infoLog.Info("starting impersonation proxy", "port", impersonationProxyPort) startImpersonatorFunc, err := c.impersonatorFunc( impersonationProxyPort, c.tlsServingCertDynamicCertProvider, @@ -402,7 +426,7 @@ func (c *impersonatorConfigController) ensureImpersonatorIsStopped(shouldCloseEr return nil } - plog.Info("Stopping impersonation proxy", "port", impersonationProxyPort) + c.infoLog.Info("stopping impersonation proxy", "port", impersonationProxyPort) close(c.serverStopCh) stopErr := <-c.errorCh @@ -416,14 +440,7 @@ func (c *impersonatorConfigController) ensureImpersonatorIsStopped(shouldCloseEr return stopErr } -func (c *impersonatorConfigController) ensureLoadBalancerIsStarted(ctx context.Context) error { - running, err := c.loadBalancerExists() - if err != nil { - return err - } - if running { - return nil - } +func (c *impersonatorConfigController) ensureLoadBalancerIsStarted(ctx context.Context, config *v1alpha1.ImpersonationProxySpec) error { appNameLabel := c.labels[appLabelKey] loadBalancer := v1.Service{ Spec: v1.ServiceSpec{ @@ -435,26 +452,21 @@ func (c *impersonatorConfigController) ensureLoadBalancerIsStarted(ctx context.C Protocol: v1.ProtocolTCP, }, }, - Selector: map[string]string{appLabelKey: appNameLabel}, + LoadBalancerIP: config.Service.LoadBalancerIP, + Selector: map[string]string{appLabelKey: appNameLabel}, }, ObjectMeta: metav1.ObjectMeta{ - Name: c.generatedLoadBalancerServiceName, - Namespace: c.namespace, - Labels: c.labels, - Annotations: map[string]string{ - "service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "4000", // AWS' default is to time out after 60 seconds idle. Prevent that. - }, + Name: c.generatedLoadBalancerServiceName, + Namespace: c.namespace, + Labels: c.labels, + Annotations: config.Service.Annotations, }, } - plog.Info("creating load balancer for impersonation proxy", - "service", c.generatedLoadBalancerServiceName, - "namespace", c.namespace) - _, err = c.k8sClient.CoreV1().Services(c.namespace).Create(ctx, &loadBalancer, metav1.CreateOptions{}) - return err + return c.createOrUpdateService(ctx, &loadBalancer) } func (c *impersonatorConfigController) ensureLoadBalancerIsStopped(ctx context.Context) error { - running, err := c.loadBalancerExists() + running, err := c.serviceExists(c.generatedLoadBalancerServiceName) if err != nil { return err } @@ -462,12 +474,82 @@ func (c *impersonatorConfigController) ensureLoadBalancerIsStopped(ctx context.C return nil } - plog.Info("Deleting load balancer for impersonation proxy", - "service", c.generatedLoadBalancerServiceName, - "namespace", c.namespace) + c.infoLog.Info("deleting load balancer for impersonation proxy", + "service", klog.KRef(c.namespace, c.generatedLoadBalancerServiceName), + ) return c.k8sClient.CoreV1().Services(c.namespace).Delete(ctx, c.generatedLoadBalancerServiceName, metav1.DeleteOptions{}) } +func (c *impersonatorConfigController) ensureClusterIPServiceIsStarted(ctx context.Context, config *v1alpha1.ImpersonationProxySpec) error { + appNameLabel := c.labels[appLabelKey] + clusterIP := v1.Service{ + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeClusterIP, + Ports: []v1.ServicePort{ + { + TargetPort: intstr.FromInt(impersonationProxyPort), + Port: defaultHTTPSPort, + Protocol: v1.ProtocolTCP, + }, + }, + Selector: map[string]string{appLabelKey: appNameLabel}, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: c.generatedClusterIPServiceName, + Namespace: c.namespace, + Labels: c.labels, + Annotations: config.Service.Annotations, + }, + } + return c.createOrUpdateService(ctx, &clusterIP) +} + +func (c *impersonatorConfigController) ensureClusterIPServiceIsStopped(ctx context.Context) error { + running, err := c.serviceExists(c.generatedClusterIPServiceName) + if err != nil { + return err + } + if !running { + return nil + } + + c.infoLog.Info("deleting cluster ip for impersonation proxy", + "service", klog.KRef(c.namespace, c.generatedClusterIPServiceName), + ) + return c.k8sClient.CoreV1().Services(c.namespace).Delete(ctx, c.generatedClusterIPServiceName, metav1.DeleteOptions{}) +} + +func (c *impersonatorConfigController) createOrUpdateService(ctx context.Context, service *v1.Service) error { + log := c.infoLog.WithValues("serviceType", service.Spec.Type, "service", klog.KObj(service)) + existing, err := c.servicesInformer.Lister().Services(c.namespace).Get(service.Name) + if k8serrors.IsNotFound(err) { + log.Info("creating service for impersonation proxy") + _, err := c.k8sClient.CoreV1().Services(c.namespace).Create(ctx, service, metav1.CreateOptions{}) + return err + } + if err != nil { + return err + } + + // Update only the specific fields that are meaningfully part of our desired state. + updated := existing.DeepCopy() + updated.ObjectMeta.Labels = service.ObjectMeta.Labels + updated.ObjectMeta.Annotations = service.ObjectMeta.Annotations + updated.Spec.LoadBalancerIP = service.Spec.LoadBalancerIP + updated.Spec.Type = service.Spec.Type + updated.Spec.Selector = service.Spec.Selector + + // If our updates didn't change anything, we're done. + if equality.Semantic.DeepEqual(existing, updated) { + return nil + } + + // Otherwise apply the updates. + c.infoLog.Info("updating service for impersonation proxy") + _, err = c.k8sClient.CoreV1().Services(c.namespace).Update(ctx, updated, metav1.UpdateOptions{}) + return err +} + func (c *impersonatorConfigController) ensureTLSSecret(ctx context.Context, nameInfo *certNameInfo, ca *certauthority.CA) error { secretFromInformer, err := c.secretsInformer.Lister().Secrets(c.namespace).Get(c.tlsSecretName) notFound := k8serrors.IsNotFound(err) @@ -494,10 +576,10 @@ func (c *impersonatorConfigController) deleteTLSSecretWhenCertificateDoesNotMatc certPEM := secret.Data[v1.TLSCertKey] block, _ := pem.Decode(certPEM) if block == nil { - plog.Warning("Found missing or not PEM-encoded data in TLS Secret", + c.infoLog.Info("found missing or not PEM-encoded data in TLS Secret", "invalidCertPEM", string(certPEM), - "secret", c.tlsSecretName, - "namespace", c.namespace) + "secret", klog.KObj(secret), + ) deleteErr := c.ensureTLSSecretIsRemoved(ctx) if deleteErr != nil { return false, fmt.Errorf("found missing or not PEM-encoded data in TLS Secret, but got error while deleting it: %w", deleteErr) @@ -507,10 +589,10 @@ func (c *impersonatorConfigController) deleteTLSSecretWhenCertificateDoesNotMatc actualCertFromSecret, err := x509.ParseCertificate(block.Bytes) if err != nil { - plog.Error("Found invalid PEM data in TLS Secret", err, + c.infoLog.Error(err, "found missing or not PEM-encoded data in TLS Secret", "invalidCertPEM", string(certPEM), - "secret", c.tlsSecretName, - "namespace", c.namespace) + "secret", klog.KObj(secret), + ) if err = c.ensureTLSSecretIsRemoved(ctx); err != nil { return false, fmt.Errorf("PEM data represented an invalid cert, but got error while deleting it: %w", err) } @@ -520,9 +602,10 @@ func (c *impersonatorConfigController) deleteTLSSecretWhenCertificateDoesNotMatc keyPEM := secret.Data[v1.TLSPrivateKeyKey] _, err = tls.X509KeyPair(certPEM, keyPEM) if err != nil { - plog.Error("Found invalid private key PEM data in TLS Secret", err, - "secret", c.tlsSecretName, - "namespace", c.namespace) + c.infoLog.Error(err, "found invalid private key PEM data in TLS Secret", + "invalidCertPEM", string(certPEM), + "secret", klog.KObj(secret), + ) if err = c.ensureTLSSecretIsRemoved(ctx); err != nil { return false, fmt.Errorf("cert had an invalid private key, but got error while deleting it: %w", err) } @@ -550,15 +633,15 @@ func (c *impersonatorConfigController) deleteTLSSecretWhenCertificateDoesNotMatc actualIPs := actualCertFromSecret.IPAddresses actualHostnames := actualCertFromSecret.DNSNames - plog.Info("Checking TLS certificate names", - "desiredIP", nameInfo.selectedIP, + c.infoLog.Info("checking TLS certificate names", + "desiredIPs", nameInfo.selectedIPs, "desiredHostname", nameInfo.selectedHostname, "actualIPs", actualIPs, "actualHostnames", actualHostnames, - "secret", c.tlsSecretName, - "namespace", c.namespace) + "secret", klog.KObj(secret), + ) - if certHostnameAndIPMatchDesiredState(nameInfo.selectedIP, actualIPs, nameInfo.selectedHostname, actualHostnames) { + if certHostnameAndIPMatchDesiredState(nameInfo.selectedIPs, actualIPs, nameInfo.selectedHostname, actualHostnames) { // The cert already matches the desired state, so there is no need to delete/recreate it. return false, nil } @@ -569,8 +652,13 @@ func (c *impersonatorConfigController) deleteTLSSecretWhenCertificateDoesNotMatc return true, nil } -func certHostnameAndIPMatchDesiredState(desiredIP net.IP, actualIPs []net.IP, desiredHostname string, actualHostnames []string) bool { - if desiredIP != nil && len(actualIPs) == 1 && desiredIP.Equal(actualIPs[0]) && len(actualHostnames) == 0 { +func certHostnameAndIPMatchDesiredState(desiredIPs []net.IP, actualIPs []net.IP, desiredHostname string, actualHostnames []string) bool { + if len(desiredIPs) > 0 && len(actualIPs) > 0 && len(actualIPs) == len(desiredIPs) && len(actualHostnames) == 0 { + for i := range desiredIPs { + if !actualIPs[i].Equal(desiredIPs[i]) { + return false + } + } return true } if desiredHostname != "" && len(actualHostnames) == 1 && desiredHostname == actualHostnames[0] && len(actualIPs) == 0 { @@ -592,7 +680,7 @@ func (c *impersonatorConfigController) ensureTLSSecretIsCreatedAndLoaded(ctx con return nil } - newTLSSecret, err := c.createNewTLSSecret(ctx, ca, nameInfo.selectedIP, nameInfo.selectedHostname) + newTLSSecret, err := c.createNewTLSSecret(ctx, ca, nameInfo.selectedIPs, nameInfo.selectedHostname) if err != nil { return err } @@ -650,9 +738,9 @@ func (c *impersonatorConfigController) createCASecret(ctx context.Context) (*cer Type: v1.SecretTypeOpaque, } - plog.Info("Creating CA certificates for impersonation proxy", - "secret", c.caSecretName, - "namespace", c.namespace) + c.infoLog.Info("creating CA certificates for impersonation proxy", + "secret", klog.KObj(&secret), + ) if _, err = c.k8sClient.CoreV1().Secrets(c.namespace).Create(ctx, &secret, metav1.CreateOptions{}); err != nil { return nil, err } @@ -660,21 +748,23 @@ func (c *impersonatorConfigController) createCASecret(ctx context.Context) (*cer return impersonationCA, nil } -func (c *impersonatorConfigController) findDesiredTLSCertificateName(config *impersonator.Config) (*certNameInfo, error) { - if config.HasEndpoint() { +func (c *impersonatorConfigController) findDesiredTLSCertificateName(config *v1alpha1.ImpersonationProxySpec) (*certNameInfo, error) { + if config.ExternalEndpoint != "" { return c.findTLSCertificateNameFromEndpointConfig(config), nil + } else if config.Service.Type == v1alpha1.ImpersonationProxyServiceTypeClusterIP { + return c.findTLSCertificateNameFromClusterIPService() } return c.findTLSCertificateNameFromLoadBalancer() } -func (c *impersonatorConfigController) findTLSCertificateNameFromEndpointConfig(config *impersonator.Config) *certNameInfo { - endpointMaybeWithPort := config.Endpoint - endpointWithoutPort := strings.Split(endpointMaybeWithPort, ":")[0] - parsedAsIP := net.ParseIP(endpointWithoutPort) - if parsedAsIP != nil { - return &certNameInfo{ready: true, selectedIP: parsedAsIP, clientEndpoint: endpointMaybeWithPort} +func (c *impersonatorConfigController) findTLSCertificateNameFromEndpointConfig(config *v1alpha1.ImpersonationProxySpec) *certNameInfo { + addr, _ := endpointaddr.Parse(config.ExternalEndpoint, 443) + endpoint := strings.TrimSuffix(addr.Endpoint(), ":443") + + if ip := net.ParseIP(addr.Host); ip != nil { + return &certNameInfo{ready: true, selectedIPs: []net.IP{ip}, clientEndpoint: endpoint} } - return &certNameInfo{ready: true, selectedHostname: endpointWithoutPort, clientEndpoint: endpointMaybeWithPort} + return &certNameInfo{ready: true, selectedHostname: addr.Host, clientEndpoint: endpoint} } func (c *impersonatorConfigController) findTLSCertificateNameFromLoadBalancer() (*certNameInfo, error) { @@ -689,9 +779,9 @@ func (c *impersonatorConfigController) findTLSCertificateNameFromLoadBalancer() } ingresses := lb.Status.LoadBalancer.Ingress if len(ingresses) == 0 || (ingresses[0].Hostname == "" && ingresses[0].IP == "") { - plog.Info("load balancer for impersonation proxy does not have an ingress yet, so skipping tls cert generation while we wait", - "service", c.generatedLoadBalancerServiceName, - "namespace", c.namespace) + c.infoLog.Info("load balancer for impersonation proxy does not have an ingress yet, so skipping tls cert generation while we wait", + "service", klog.KObj(lb), + ) return &certNameInfo{ready: false}, nil } for _, ingress := range ingresses { @@ -704,22 +794,45 @@ func (c *impersonatorConfigController) findTLSCertificateNameFromLoadBalancer() ip := ingress.IP parsedIP := net.ParseIP(ip) if parsedIP != nil { - return &certNameInfo{ready: true, selectedIP: parsedIP, clientEndpoint: ip}, nil + return &certNameInfo{ready: true, selectedIPs: []net.IP{parsedIP}, clientEndpoint: ip}, nil } } return nil, fmt.Errorf("could not find valid IP addresses or hostnames from load balancer %s/%s", c.namespace, lb.Name) } -func (c *impersonatorConfigController) createNewTLSSecret(ctx context.Context, ca *certauthority.CA, ip net.IP, hostname string) (*v1.Secret, error) { +func (c *impersonatorConfigController) findTLSCertificateNameFromClusterIPService() (*certNameInfo, error) { + clusterIP, err := c.servicesInformer.Lister().Services(c.namespace).Get(c.generatedClusterIPServiceName) + notFound := k8serrors.IsNotFound(err) + if notFound { + // We aren't ready and will try again later in this case. + return &certNameInfo{ready: false}, nil + } + if err != nil { + return nil, err + } + ip := clusterIP.Spec.ClusterIP + ips := clusterIP.Spec.ClusterIPs + if ip != "" { + // clusterIP will always exist when clusterIPs does, but not vice versa + var parsedIPs []net.IP + if len(ips) > 0 { + for _, ipFromIPs := range ips { + parsedIPs = append(parsedIPs, net.ParseIP(ipFromIPs)) + } + } else { + parsedIPs = []net.IP{net.ParseIP(ip)} + } + return &certNameInfo{ready: true, selectedIPs: parsedIPs, clientEndpoint: ip}, nil + } + return &certNameInfo{ready: false}, nil +} + +func (c *impersonatorConfigController) createNewTLSSecret(ctx context.Context, ca *certauthority.CA, ips []net.IP, hostname string) (*v1.Secret, error) { var hostnames []string - var ips []net.IP if hostname != "" { hostnames = []string{hostname} } - if ip != nil { - ips = []net.IP{ip} - } impersonationCert, err := ca.IssueServerCert(hostnames, ips, approximatelyOneHundredYears) if err != nil { @@ -744,11 +857,11 @@ func (c *impersonatorConfigController) createNewTLSSecret(ctx context.Context, c Type: v1.SecretTypeTLS, } - plog.Info("Creating TLS certificates for impersonation proxy", + c.infoLog.Info("creating TLS certificates for impersonation proxy", "ips", ips, "hostnames", hostnames, - "secret", c.tlsSecretName, - "namespace", c.namespace) + "secret", klog.KObj(newTLSSecret), + ) return c.k8sClient.CoreV1().Secrets(c.namespace).Create(ctx, newTLSSecret, metav1.CreateOptions{}) } @@ -761,10 +874,10 @@ func (c *impersonatorConfigController) loadTLSCertFromSecret(tlsSecret *v1.Secre return fmt.Errorf("could not parse TLS cert PEM data from Secret: %w", err) } - plog.Info("Loading TLS certificates for impersonation proxy", + c.infoLog.Info("loading TLS certificates for impersonation proxy", "certPEM", string(certPEM), - "secret", c.tlsSecretName, - "namespace", c.namespace) + "secret", klog.KObj(tlsSecret), + ) return nil } @@ -777,9 +890,9 @@ func (c *impersonatorConfigController) ensureTLSSecretIsRemoved(ctx context.Cont if !tlsSecretExists { return nil } - plog.Info("Deleting TLS certificates for impersonation proxy", - "secret", c.tlsSecretName, - "namespace", c.namespace) + c.infoLog.Info("deleting TLS certificates for impersonation proxy", + "secret", klog.KRef(c.namespace, c.tlsSecretName), + ) err = c.k8sClient.CoreV1().Secrets(c.namespace).Delete(ctx, c.tlsSecretName, metav1.DeleteOptions{}) notFound := k8serrors.IsNotFound(err) if notFound { @@ -815,20 +928,20 @@ func (c *impersonatorConfigController) loadSignerCA(status v1alpha1.StrategyStat return fmt.Errorf("could not load the impersonator's credential signing secret: %w", err) } - plog.Info("Loading credential signing certificate for impersonation proxy", + c.infoLog.Info("loading credential signing certificate for impersonation proxy", "certPEM", string(certPEM), - "fromSecret", c.impersonationSignerSecretName, - "namespace", c.namespace) + "secret", klog.KObj(signingCertSecret), + ) return nil } func (c *impersonatorConfigController) clearSignerCA() { - plog.Info("Clearing credential signing certificate for impersonation proxy") + c.debugLog.Info("clearing credential signing certificate for impersonation proxy") c.impersonationSigningCertProvider.UnsetCertKeyContent() } -func (c *impersonatorConfigController) doSyncResult(nameInfo *certNameInfo, config *impersonator.Config, ca *certauthority.CA) *v1alpha1.CredentialIssuerStrategy { +func (c *impersonatorConfigController) doSyncResult(nameInfo *certNameInfo, config *v1alpha1.ImpersonationProxySpec, ca *certauthority.CA) *v1alpha1.CredentialIssuerStrategy { switch { case c.disabledExplicitly(config): return &v1alpha1.CredentialIssuerStrategy{ @@ -871,3 +984,46 @@ func (c *impersonatorConfigController) doSyncResult(nameInfo *certNameInfo, conf } } } + +func validateCredentialIssuerSpec(spec *v1alpha1.ImpersonationProxySpec) error { + // Validate that the mode is one of our known values. + switch spec.Mode { + case v1alpha1.ImpersonationProxyModeDisabled: + case v1alpha1.ImpersonationProxyModeAuto: + case v1alpha1.ImpersonationProxyModeEnabled: + default: + return fmt.Errorf("invalid proxy mode %q (expected auto, disabled, or enabled)", spec.Mode) + } + + // If disabled, ignore all other fields and consider the configuration valid. + if spec.Mode == v1alpha1.ImpersonationProxyModeDisabled { + return nil + } + + // Validate that the service type is one of our known values. + switch spec.Service.Type { + case v1alpha1.ImpersonationProxyServiceTypeNone: + case v1alpha1.ImpersonationProxyServiceTypeLoadBalancer: + case v1alpha1.ImpersonationProxyServiceTypeClusterIP: + default: + return fmt.Errorf("invalid service type %q (expected None, LoadBalancer, or ClusterIP)", spec.Service.Type) + } + + // If specified, validate that the LoadBalancerIP is a valid IPv4 or IPv6 address. + if ip := spec.Service.LoadBalancerIP; ip != "" && len(validation.IsValidIP(ip)) > 0 { + return fmt.Errorf("invalid LoadBalancerIP %q", spec.Service.LoadBalancerIP) + } + + // If service is type "None", a non-empty external endpoint must be specified. + if spec.ExternalEndpoint == "" && spec.Service.Type == v1alpha1.ImpersonationProxyServiceTypeNone { + return fmt.Errorf("externalEndpoint must be set when service.type is None") + } + + if spec.ExternalEndpoint != "" { + if _, err := endpointaddr.Parse(spec.ExternalEndpoint, 443); err != nil { + return fmt.Errorf("invalid ExternalEndpoint %q: %w", spec.ExternalEndpoint, err) + } + } + + return nil +} diff --git a/internal/controller/impersonatorconfig/impersonator_config_test.go b/internal/controller/impersonatorconfig/impersonator_config_test.go index c7f7767a..91db0a76 100644 --- a/internal/controller/impersonatorconfig/impersonator_config_test.go +++ b/internal/controller/impersonatorconfig/impersonator_config_test.go @@ -25,61 +25,66 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/clock" + "k8s.io/apimachinery/pkg/util/intstr" kubeinformers "k8s.io/client-go/informers" kubernetesfake "k8s.io/client-go/kubernetes/fake" coretesting "k8s.io/client-go/testing" "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" pinnipedfake "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake" + pinnipedinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions" "go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/controller/apicerts" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/dynamiccert" "go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/testutil" + "go.pinniped.dev/internal/testutil/testlogger" ) func TestImpersonatorConfigControllerOptions(t *testing.T) { spec.Run(t, "options", func(t *testing.T, when spec.G, it spec.S) { const installedInNamespace = "some-namespace" - const configMapResourceName = "some-configmap-resource-name" + const credentialIssuerResourceName = "some-credential-issuer-resource-name" const generatedLoadBalancerServiceName = "some-service-resource-name" + const generatedClusterIPServiceName = "some-cluster-ip-resource-name" const tlsSecretName = "some-tls-secret-name" //nolint:gosec // this is not a credential const caSecretName = "some-ca-secret-name" const caSignerName = "some-ca-signer-name" var r *require.Assertions var observableWithInformerOption *testutil.ObservableWithInformerOption - var observableWithInitialEventOption *testutil.ObservableWithInitialEventOption - var configMapsInformerFilter controllerlib.Filter + var credIssuerInformerFilter controllerlib.Filter var servicesInformerFilter controllerlib.Filter var secretsInformerFilter controllerlib.Filter + var testLog *testlogger.Logger it.Before(func() { r = require.New(t) observableWithInformerOption = testutil.NewObservableWithInformerOption() - observableWithInitialEventOption = testutil.NewObservableWithInitialEventOption() + pinnipedInformerFactory := pinnipedinformers.NewSharedInformerFactory(nil, 0) sharedInformerFactory := kubeinformers.NewSharedInformerFactory(nil, 0) - configMapsInformer := sharedInformerFactory.Core().V1().ConfigMaps() + credIssuerInformer := pinnipedInformerFactory.Config().V1alpha1().CredentialIssuers() servicesInformer := sharedInformerFactory.Core().V1().Services() secretsInformer := sharedInformerFactory.Core().V1().Secrets() + testLog = testlogger.New(t) _ = NewImpersonatorConfigController( installedInNamespace, - configMapResourceName, - "", + credentialIssuerResourceName, nil, nil, - configMapsInformer, + credIssuerInformer, servicesInformer, secretsInformer, observableWithInformerOption.WithInformer, - observableWithInitialEventOption.WithInitialEvent, generatedLoadBalancerServiceName, + generatedClusterIPServiceName, tlsSecretName, caSecretName, nil, @@ -87,58 +92,41 @@ func TestImpersonatorConfigControllerOptions(t *testing.T) { nil, caSignerName, nil, + testLog, ) - configMapsInformerFilter = observableWithInformerOption.GetFilterForInformer(configMapsInformer) + credIssuerInformerFilter = observableWithInformerOption.GetFilterForInformer(credIssuerInformer) servicesInformerFilter = observableWithInformerOption.GetFilterForInformer(servicesInformer) secretsInformerFilter = observableWithInformerOption.GetFilterForInformer(secretsInformer) }) - when("watching ConfigMap objects", func() { + when("watching CredentialIssuer objects", func() { var subject controllerlib.Filter - var target, wrongNamespace, wrongName, unrelated *corev1.ConfigMap + var target, wrongName, otherWrongName *v1alpha1.CredentialIssuer it.Before(func() { - subject = configMapsInformerFilter - target = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: configMapResourceName, Namespace: installedInNamespace}} - wrongNamespace = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: configMapResourceName, Namespace: "wrong-namespace"}} - wrongName = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: installedInNamespace}} - unrelated = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: "wrong-namespace"}} + subject = credIssuerInformerFilter + target = &v1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}} + wrongName = &v1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "wrong-name"}} + otherWrongName = &v1alpha1.CredentialIssuer{ObjectMeta: metav1.ObjectMeta{Name: "other-wrong-name"}} }) - when("the target ConfigMap changes", func() { + when("the target CredentialIssuer changes", func() { it("returns true to trigger the sync method", func() { r.True(subject.Add(target)) - r.True(subject.Update(target, unrelated)) - r.True(subject.Update(unrelated, target)) + r.True(subject.Update(target, wrongName)) + r.True(subject.Update(wrongName, target)) r.True(subject.Delete(target)) }) }) - when("a ConfigMap from another namespace changes", func() { - it("returns false to avoid triggering the sync method", func() { - r.False(subject.Add(wrongNamespace)) - r.False(subject.Update(wrongNamespace, unrelated)) - r.False(subject.Update(unrelated, wrongNamespace)) - r.False(subject.Delete(wrongNamespace)) - }) - }) - - when("a ConfigMap with a different name changes", func() { + when("a CredentialIssuer with a different name changes", func() { it("returns false to avoid triggering the sync method", func() { r.False(subject.Add(wrongName)) - r.False(subject.Update(wrongName, unrelated)) - r.False(subject.Update(unrelated, wrongName)) + r.False(subject.Update(wrongName, otherWrongName)) + r.False(subject.Update(otherWrongName, wrongName)) r.False(subject.Delete(wrongName)) }) }) - - when("a ConfigMap with a different name and a different namespace changes", func() { - it("returns false to avoid triggering the sync method", func() { - r.False(subject.Add(unrelated)) - r.False(subject.Update(unrelated, unrelated)) - r.False(subject.Delete(unrelated)) - }) - }) }) when("watching Service objects", func() { @@ -251,15 +239,6 @@ func TestImpersonatorConfigControllerOptions(t *testing.T) { }) }) }) - - when("starting up", func() { - it("asks for an initial event because the ConfigMap may not exist yet and it needs to run anyway", func() { - r.Equal(&controllerlib.Key{ - Namespace: installedInNamespace, - Name: configMapResourceName, - }, observableWithInitialEventOption.GetInitialEventKey()) - }) - }) }, spec.Parallel(), spec.Report(report.Terminal{})) } @@ -267,9 +246,9 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { name := t.Name() spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) { const installedInNamespace = "some-namespace" - const configMapResourceName = "some-configmap-resource-name" const credentialIssuerResourceName = "some-credential-issuer-resource-name" const loadBalancerServiceName = "some-service-resource-name" + const clusterIPServiceName = "some-cluster-ip-resource-name" const tlsSecretName = "some-tls-secret-name" //nolint:gosec // this is not a credential const caSecretName = "some-ca-secret-name" const caSignerName = "some-ca-signer-name" @@ -283,6 +262,8 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { var subject controllerlib.Controller var kubeAPIClient *kubernetesfake.Clientset var pinnipedAPIClient *pinnipedfake.Clientset + var pinnipedInformerClient *pinnipedfake.Clientset + var pinnipedInformers pinnipedinformers.SharedInformerFactory var kubeInformerClient *kubernetesfake.Clientset var kubeInformers kubeinformers.SharedInformerFactory var cancelContext context.Context @@ -302,6 +283,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { var testHTTPServerInterruptCh chan struct{} var queue *testQueue var validClientCert *tls.Certificate + var testLog *testlogger.Logger var impersonatorFunc = func( port int, @@ -441,7 +423,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { t.Logf("DialContext replacing addr %s with %s", addr, replacementAddr) addr = replacementAddr } else if dnsOverrides != nil { - t.Fatal("dnsOverrides was provided but not used, which was probably a mistake") + t.Fatalf("dnsOverrides was provided but not used, which was probably a mistake. addr %s", addr) } return realDialer.DialContext(ctx, network, addr) } @@ -533,16 +515,15 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { // Set this at the last second to allow for injection of server override. subject = NewImpersonatorConfigController( installedInNamespace, - configMapResourceName, credentialIssuerResourceName, kubeAPIClient, pinnipedAPIClient, - kubeInformers.Core().V1().ConfigMaps(), + pinnipedInformers.Config().V1alpha1().CredentialIssuers(), kubeInformers.Core().V1().Services(), kubeInformers.Core().V1().Secrets(), controllerlib.WithInformer, - controllerlib.WithInitialEvent, loadBalancerServiceName, + clusterIPServiceName, tlsSecretName, caSecretName, labels, @@ -550,6 +531,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { impersonatorFunc, caSignerName, signingCertProvider, + testLog, ) // Set this at the last second to support calling subject.Name(). @@ -557,28 +539,21 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { Context: cancelContext, Name: subject.Name(), Key: controllerlib.Key{ - Namespace: installedInNamespace, - Name: configMapResourceName, + Name: credentialIssuerResourceName, }, Queue: queue, } // Must start informers before calling TestRunSynchronously() kubeInformers.Start(cancelContext.Done()) + pinnipedInformers.Start(cancelContext.Done()) controllerlib.TestRunSynchronously(t, subject) } - var addImpersonatorConfigMapToTracker = func(resourceName, configYAML string, client *kubernetesfake.Clientset) { - impersonatorConfigMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: resourceName, - Namespace: installedInNamespace, - }, - Data: map[string]string{ - "config.yaml": configYAML, - }, - } - r.NoError(client.Tracker().Add(impersonatorConfigMap)) + var addCredentialIssuerToTrackers = func(credIssuer v1alpha1.CredentialIssuer, informerClient *pinnipedfake.Clientset, mainClient *pinnipedfake.Clientset) { + t.Logf("adding CredentialIssuer %s to informer and main clientsets", credIssuer.Name) + r.NoError(informerClient.Tracker().Add(&credIssuer)) + r.NoError(mainClient.Tracker().Add(&credIssuer)) } var newSecretWithData = func(resourceName string, data map[string][]byte) *corev1.Secret { @@ -645,14 +620,35 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: installedInNamespace, + Labels: labels, }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeLoadBalancer, + Ports: []corev1.ServicePort{ + { + TargetPort: intstr.FromInt(impersonationProxyPort), + Port: defaultHTTPSPort, + Protocol: corev1.ProtocolTCP, + }, + }, + Selector: map[string]string{appLabelKey: labels[appLabelKey]}, }, Status: status, } } + var newClusterIPService = func(resourceName string, status corev1.ServiceStatus, spec corev1.ServiceSpec) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: installedInNamespace, + Labels: labels, + }, + Spec: spec, + Status: status, + } + } + // Anytime an object is added/updated/deleted in the informer's client *after* the informer is started, then we // need to wait for the informer's cache to asynchronously pick up that change from its "watch". // If an object is added to the informer's client *before* the informer is started, then waiting is @@ -670,6 +666,19 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { r.Equal(obj, objFromInformer, "was waiting for expected to be found in informer, but found actual") } + var waitForClusterScopedObjectToAppearInInformer = func(obj kubeclient.Object, informer controllerlib.InformerGetter) { + var objFromInformer interface{} + var exists bool + var err error + assert.Eventually(t, func() bool { + objFromInformer, exists, err = informer.Informer().GetIndexer().GetByKey(obj.GetName()) + return err == nil && exists && reflect.DeepEqual(objFromInformer.(kubeclient.Object), obj) + }, 30*time.Second, 10*time.Millisecond) + r.NoError(err) + r.True(exists, "this object should have existed in informer but didn't: %+v", obj) + r.Equal(obj, objFromInformer, "was waiting for expected to be found in informer, but found actual") + } + // See comment for waitForObjectToAppearInInformer above. var waitForObjectToBeDeletedFromInformer = func(resourceName string, informer controllerlib.InformerGetter) { var objFromInformer interface{} @@ -683,7 +692,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { r.False(exists, "this object should have been deleted from informer but wasn't: %s", objFromInformer) } - var addObjectToInformerAndWait = func(obj kubeclient.Object, informer controllerlib.InformerGetter) { + var addObjectToKubeInformerAndWait = func(obj kubeclient.Object, informer controllerlib.InformerGetter) { r.NoError(kubeInformerClient.Tracker().Add(obj)) waitForObjectToAppearInInformer(obj, informer) } @@ -691,27 +700,19 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { var addObjectFromCreateActionToInformerAndWait = func(action coretesting.Action, informer controllerlib.InformerGetter) { createdObject, ok := action.(coretesting.CreateAction).GetObject().(kubeclient.Object) r.True(ok, "should have been able to cast this action's object to kubeclient.Object: %v", action) - addObjectToInformerAndWait(createdObject, informer) + addObjectToKubeInformerAndWait(createdObject, informer) } - var updateImpersonatorConfigMapInInformerAndWait = func(resourceName, configYAML string, informer controllerlib.InformerGetter) { - configMapObj, err := kubeInformerClient.Tracker().Get( - schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, - installedInNamespace, - resourceName, - ) - r.NoError(err) - configMap := configMapObj.(*corev1.ConfigMap) - configMap = configMap.DeepCopy() // don't edit the original from the tracker - configMap.Data = map[string]string{ - "config.yaml": configYAML, - } - r.NoError(kubeInformerClient.Tracker().Update( - schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, - configMap, - installedInNamespace, - )) - waitForObjectToAppearInInformer(configMap, informer) + var updateCredentialIssuerInInformerAndWait = func(resourceName string, credIssuerSpec v1alpha1.CredentialIssuerSpec, informer controllerlib.InformerGetter) { + credIssuersGVR := v1alpha1.Resource("credentialissuers").WithVersion("v1alpha1") + credIssuerObj, err := pinnipedInformerClient.Tracker().Get(credIssuersGVR, "", resourceName) + r.NoError(err, "could not find CredentialIssuer to update for test") + + credIssuer := credIssuerObj.(*v1alpha1.CredentialIssuer) + credIssuer = credIssuer.DeepCopy() // don't edit the original from the tracker + credIssuer.Spec = credIssuerSpec + r.NoError(pinnipedInformerClient.Tracker().Update(credIssuersGVR, credIssuer, "")) + waitForClusterScopedObjectToAppearInInformer(credIssuer, informer) } var updateLoadBalancerServiceInInformerAndWait = func(resourceName string, ingresses []corev1.LoadBalancerIngress, informer controllerlib.InformerGetter) { @@ -744,6 +745,39 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { r.NoError(client.Tracker().Add(loadBalancerService)) } + var addClusterIPServiceToTracker = func(resourceName string, clusterIP string, client *kubernetesfake.Clientset) { + clusterIPService := newClusterIPService(resourceName, corev1.ServiceStatus{}, corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: clusterIP, + Ports: []corev1.ServicePort{ + { + TargetPort: intstr.FromInt(impersonationProxyPort), + Port: defaultHTTPSPort, + Protocol: corev1.ProtocolTCP, + }, + }, + Selector: map[string]string{appLabelKey: labels[appLabelKey]}, + }) + r.NoError(client.Tracker().Add(clusterIPService)) + } + + var addDualStackClusterIPServiceToTracker = func(resourceName string, clusterIP0 string, clusterIP1 string, client *kubernetesfake.Clientset) { + clusterIPService := newClusterIPService(resourceName, corev1.ServiceStatus{}, corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: clusterIP0, + ClusterIPs: []string{clusterIP0, clusterIP1}, + Ports: []corev1.ServicePort{ + { + TargetPort: intstr.FromInt(impersonationProxyPort), + Port: defaultHTTPSPort, + Protocol: corev1.ProtocolTCP, + }, + }, + Selector: map[string]string{appLabelKey: labels[appLabelKey]}, + }) + r.NoError(client.Tracker().Add(clusterIPService)) + } + var addSecretToTrackers = func(secret *corev1.Secret, clients ...*kubernetesfake.Clientset) { for _, client := range clients { secretCopy := secret.DeepCopy() @@ -823,17 +857,21 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { return s } - var newPendingStrategy = func() v1alpha1.CredentialIssuerStrategy { + var newPendingStrategy = func(msg string) v1alpha1.CredentialIssuerStrategy { return v1alpha1.CredentialIssuerStrategy{ Type: v1alpha1.ImpersonationProxyStrategyType, Status: v1alpha1.ErrorStrategyStatus, Reason: v1alpha1.PendingStrategyReason, - Message: "waiting for load balancer Service to be assigned IP or hostname", + Message: msg, LastUpdateTime: metav1.NewTime(frozenNow), Frontend: nil, } } + var newPendingStrategyWaitingForLB = func() v1alpha1.CredentialIssuerStrategy { + return newPendingStrategy("waiting for load balancer Service to be assigned IP or hostname") + } + var newErrorStrategy = func(msg string) v1alpha1.CredentialIssuerStrategy { return v1alpha1.CredentialIssuerStrategy{ Type: v1alpha1.ImpersonationProxyStrategyType, @@ -868,11 +906,18 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { // As long as we get the final result that we wanted then we are happy for the purposes // of this test. credentialIssuer := getCredentialIssuer() - r.Equal(labels, credentialIssuer.Labels) r.Equal([]v1alpha1.CredentialIssuerStrategy{expectedStrategy}, credentialIssuer.Status.Strategies) } - var requireLoadBalancerWasCreated = func(action coretesting.Action) { + var requireServiceWasDeleted = func(action coretesting.Action, serviceName string) { + deleteAction, ok := action.(coretesting.DeleteAction) + r.True(ok, "should have been able to cast this action to DeleteAction: %v", action) + r.Equal("delete", deleteAction.GetVerb()) + r.Equal(serviceName, deleteAction.GetName()) + r.Equal("services", deleteAction.GetResource().Resource) + } + + var requireLoadBalancerWasCreated = func(action coretesting.Action) *corev1.Service { createAction, ok := action.(coretesting.CreateAction) r.True(ok, "should have been able to cast this action to CreateAction: %v", action) r.Equal("create", createAction.GetVerb()) @@ -882,15 +927,45 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { r.Equal(corev1.ServiceTypeLoadBalancer, createdLoadBalancerService.Spec.Type) r.Equal("app-name", createdLoadBalancerService.Spec.Selector["app"]) r.Equal(labels, createdLoadBalancerService.Labels) - r.Equal(map[string]string{"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "4000"}, createdLoadBalancerService.Annotations) + return createdLoadBalancerService } - var requireLoadBalancerWasDeleted = func(action coretesting.Action) { - deleteAction, ok := action.(coretesting.DeleteAction) - r.True(ok, "should have been able to cast this action to DeleteAction: %v", action) - r.Equal("delete", deleteAction.GetVerb()) - r.Equal(loadBalancerServiceName, deleteAction.GetName()) - r.Equal("services", deleteAction.GetResource().Resource) + var requireLoadBalancerWasUpdated = func(action coretesting.Action) *corev1.Service { + updateAction, ok := action.(coretesting.UpdateAction) + r.True(ok, "should have been able to cast this action to UpdateAction: %v", action) + r.Equal("update", updateAction.GetVerb()) + updatedLoadBalancerService := updateAction.GetObject().(*corev1.Service) + r.Equal(loadBalancerServiceName, updatedLoadBalancerService.Name) + r.Equal(installedInNamespace, updatedLoadBalancerService.Namespace) + r.Equal(corev1.ServiceTypeLoadBalancer, updatedLoadBalancerService.Spec.Type) + r.Equal("app-name", updatedLoadBalancerService.Spec.Selector["app"]) + r.Equal(labels, updatedLoadBalancerService.Labels) + return updatedLoadBalancerService + } + + var requireClusterIPWasCreated = func(action coretesting.Action) *corev1.Service { + createAction, ok := action.(coretesting.CreateAction) + r.True(ok, "should have been able to cast this action to CreateAction: %v", action) + r.Equal("create", createAction.GetVerb()) + createdClusterIPService := createAction.GetObject().(*corev1.Service) + r.Equal(clusterIPServiceName, createdClusterIPService.Name) + r.Equal(corev1.ServiceTypeClusterIP, createdClusterIPService.Spec.Type) + r.Equal("app-name", createdClusterIPService.Spec.Selector["app"]) + r.Equal(labels, createdClusterIPService.Labels) + return createdClusterIPService + } + + var requireClusterIPWasUpdated = func(action coretesting.Action) *corev1.Service { + updateAction, ok := action.(coretesting.UpdateAction) + r.True(ok, "should have been able to cast this action to UpdateAction: %v", action) + r.Equal("update", updateAction.GetVerb()) + updatedLoadBalancerService := updateAction.GetObject().(*corev1.Service) + r.Equal(clusterIPServiceName, updatedLoadBalancerService.Name) + r.Equal(installedInNamespace, updatedLoadBalancerService.Namespace) + r.Equal(corev1.ServiceTypeClusterIP, updatedLoadBalancerService.Spec.Type) + r.Equal("app-name", updatedLoadBalancerService.Spec.Selector["app"]) + r.Equal(labels, updatedLoadBalancerService.Labels) + return updatedLoadBalancerService } var requireTLSSecretWasDeleted = func(action coretesting.Action) { @@ -968,6 +1043,10 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { r = require.New(t) queue = &testQueue{} cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background()) + + pinnipedInformerClient = pinnipedfake.NewSimpleClientset() + pinnipedInformers = pinnipedinformers.NewSharedInformerFactoryWithOptions(pinnipedInformerClient, 0) + kubeInformerClient = kubernetesfake.NewSimpleClientset() kubeInformers = kubeinformers.NewSharedInformerFactoryWithOptions(kubeInformerClient, 0, kubeinformers.WithNamespace(installedInNamespace), @@ -985,6 +1064,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { signingCASecret = newSigningKeySecret(caSignerName, signingCACertPEM, signingCAKeyPEM) validClientCert, err = ca.IssueClientCert("username", nil, time.Hour) r.NoError(err) + testLog = testlogger.New(t) }) it.After(func() { @@ -992,10 +1072,91 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { closeTestHTTPServer() }) - when("the ConfigMap does not yet exist in the installation namespace or it was deleted (defaults to auto mode)", func() { + when("the CredentialIssuer does not yet exist or it was deleted (sync returns an error)", func() { it.Before(func() { addSecretToTrackers(signingCASecret, kubeInformerClient) - addImpersonatorConfigMapToTracker("some-other-unrelated-configmap", "foo: bar", kubeInformerClient) + }) + + when("there are visible control plane nodes and a loadbalancer and a tls Secret", func() { + it.Before(func() { + addNodeWithRoleToTracker("control-plane", kubeAPIClient) + addLoadBalancerServiceToTracker(loadBalancerServiceName, kubeInformerClient) + addLoadBalancerServiceToTracker(loadBalancerServiceName, kubeAPIClient) + addSecretToTrackers(newEmptySecret(tlsSecretName), kubeAPIClient, kubeInformerClient) + }) + + it("errors and does nothing else", func() { + startInformersAndController() + r.EqualError(runControllerSync(), `could not get CredentialIssuer to update: credentialissuer.config.concierge.pinniped.dev "some-credential-issuer-resource-name" not found`) + requireTLSServerWasNeverStarted() + r.Len(kubeAPIClient.Actions(), 0) + }) + }) + }) + + when("the configuration is auto mode with an endpoint and service type none", func() { + it.Before(func() { + addSecretToTrackers(signingCASecret, kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeAuto, + ExternalEndpoint: localhostIP, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + }) + + when("there are visible control plane nodes", func() { + it.Before(func() { + addNodeWithRoleToTracker("control-plane", kubeAPIClient) + }) + + it("does not start the impersonator", func() { + startInformersAndController() + r.NoError(runControllerSync()) + requireTLSServerWasNeverStarted() + requireNodesListed(kubeAPIClient.Actions()[0]) + r.Len(kubeAPIClient.Actions(), 1) + requireCredentialIssuer(newAutoDisabledStrategy()) + requireSigningCertProviderIsEmpty() + }) + }) + + when("there are not visible control plane nodes", func() { + it.Before(func() { + addNodeWithRoleToTracker("worker", kubeAPIClient) + }) + + it("starts the impersonator according to the settings in the CredentialIssuer", func() { + startInformersAndController() + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 3) + requireNodesListed(kubeAPIClient.Actions()[0]) + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) + requireTLSServerIsRunning(ca, testServerAddr(), nil) + requireCredentialIssuer(newSuccessStrategy(localhostIP, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + }) + }) + + when("the configuration is auto mode", func() { + it.Before(func() { + addSecretToTrackers(signingCASecret, kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeAuto, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) }) when("there are visible control plane nodes", func() { @@ -1028,7 +1189,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { requireTLSServerWasNeverStarted() r.Len(kubeAPIClient.Actions(), 3) requireNodesListed(kubeAPIClient.Actions()[0]) - requireLoadBalancerWasDeleted(kubeAPIClient.Actions()[1]) + requireServiceWasDeleted(kubeAPIClient.Actions()[1], loadBalancerServiceName) requireTLSSecretWasDeleted(kubeAPIClient.Actions()[2]) requireCredentialIssuer(newAutoDisabledStrategy()) requireSigningCertProviderIsEmpty() @@ -1048,7 +1209,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { requireNodesListed(kubeAPIClient.Actions()[0]) requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) requireCASecretWasCreated(kubeAPIClient.Actions()[2]) - requireCredentialIssuer(newPendingStrategy()) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) requireSigningCertProviderIsEmpty() }) }) @@ -1067,7 +1228,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { r.Len(kubeAPIClient.Actions(), 2) requireNodesListed(kubeAPIClient.Actions()[0]) requireCASecretWasCreated(kubeAPIClient.Actions()[1]) - requireCredentialIssuer(newPendingStrategy()) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) requireSigningCertProviderIsEmpty() }) }) @@ -1086,7 +1247,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { r.Len(kubeAPIClient.Actions(), 2) requireNodesListed(kubeAPIClient.Actions()[0]) requireCASecretWasCreated(kubeAPIClient.Actions()[1]) - requireCredentialIssuer(newPendingStrategy()) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) requireSigningCertProviderIsEmpty() }) }) @@ -1286,421 +1447,692 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { }) }) - when("the ConfigMap is already in the installation namespace", func() { + when("the configuration is disabled mode", func() { + it.Before(func() { + addSecretToTrackers(signingCASecret, kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeDisabled, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + }) + + it("does not start the impersonator", func() { + startInformersAndController() + r.NoError(runControllerSync()) + requireTLSServerWasNeverStarted() + requireNodesListed(kubeAPIClient.Actions()[0]) + r.Len(kubeAPIClient.Actions(), 1) + requireCredentialIssuer(newManuallyDisabledStrategy()) + requireSigningCertProviderIsEmpty() + }) + }) + + when("the configuration is enabled mode", func() { it.Before(func() { addSecretToTrackers(signingCASecret, kubeInformerClient) }) - - when("the configuration is auto mode with an endpoint", func() { + when("no load balancer", func() { it.Before(func() { - configMapYAML := fmt.Sprintf("{mode: auto, endpoint: %s}", localhostIP) - addImpersonatorConfigMapToTracker(configMapResourceName, configMapYAML, kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("control-plane", kubeAPIClient) }) - when("there are visible control plane nodes", func() { - it.Before(func() { - addNodeWithRoleToTracker("control-plane", kubeAPIClient) - }) - - it("does not start the impersonator", func() { - startInformersAndController() - r.NoError(runControllerSync()) - requireTLSServerWasNeverStarted() - requireNodesListed(kubeAPIClient.Actions()[0]) - r.Len(kubeAPIClient.Actions(), 1) - requireCredentialIssuer(newAutoDisabledStrategy()) - requireSigningCertProviderIsEmpty() - }) - }) - - when("there are not visible control plane nodes", func() { - it.Before(func() { - addNodeWithRoleToTracker("worker", kubeAPIClient) - }) - - it("starts the impersonator according to the settings in the ConfigMap", func() { - startInformersAndController() - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 3) - requireNodesListed(kubeAPIClient.Actions()[0]) - ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) - requireTLSServerIsRunning(ca, testServerAddr(), nil) - requireCredentialIssuer(newSuccessStrategy(localhostIP, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - }) - }) - }) - - when("the configuration is disabled mode", func() { - it.Before(func() { - addImpersonatorConfigMapToTracker(configMapResourceName, "mode: disabled", kubeInformerClient) - addNodeWithRoleToTracker("worker", kubeAPIClient) - }) - - it("does not start the impersonator", func() { + it("starts the impersonator and creates a load balancer", func() { startInformersAndController() r.NoError(runControllerSync()) - requireTLSServerWasNeverStarted() + r.Len(kubeAPIClient.Actions(), 3) requireNodesListed(kubeAPIClient.Actions()[0]) - r.Len(kubeAPIClient.Actions(), 1) - requireCredentialIssuer(newManuallyDisabledStrategy()) + requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) + requireCASecretWasCreated(kubeAPIClient.Actions()[2]) + requireTLSServerIsRunningWithoutCerts() + requireCredentialIssuer(newPendingStrategyWaitingForLB()) + requireSigningCertProviderIsEmpty() + }) + + it("returns an error when the impersonation TLS server fails to start", func() { + impersonatorFuncError = errors.New("impersonation server start error") + startInformersAndController() + r.EqualError(runControllerSync(), "impersonation server start error") + requireCredentialIssuer(newErrorStrategy("impersonation server start error")) requireSigningCertProviderIsEmpty() }) }) - when("the configuration is enabled mode", func() { - when("no load balancer", func() { - it.Before(func() { - addImpersonatorConfigMapToTracker(configMapResourceName, "mode: enabled", kubeInformerClient) - addNodeWithRoleToTracker("control-plane", kubeAPIClient) - }) - - it("starts the impersonator and creates a load balancer", func() { - startInformersAndController() - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 3) - requireNodesListed(kubeAPIClient.Actions()[0]) - requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) - requireCASecretWasCreated(kubeAPIClient.Actions()[2]) - requireTLSServerIsRunningWithoutCerts() - requireCredentialIssuer(newPendingStrategy()) - requireSigningCertProviderIsEmpty() - }) - - it("returns an error when the impersonation TLS server fails to start", func() { - impersonatorFuncError = errors.New("impersonation server start error") - startInformersAndController() - r.EqualError(runControllerSync(), "impersonation server start error") - requireCredentialIssuer(newErrorStrategy("impersonation server start error")) - requireSigningCertProviderIsEmpty() - }) + when("a loadbalancer already exists", func() { + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + addLoadBalancerServiceToTracker(loadBalancerServiceName, kubeInformerClient) + addLoadBalancerServiceToTracker(loadBalancerServiceName, kubeAPIClient) }) - when("a loadbalancer already exists", func() { - it.Before(func() { - addImpersonatorConfigMapToTracker(configMapResourceName, "mode: enabled", kubeInformerClient) - addNodeWithRoleToTracker("worker", kubeAPIClient) - addLoadBalancerServiceToTracker(loadBalancerServiceName, kubeInformerClient) - addLoadBalancerServiceToTracker(loadBalancerServiceName, kubeAPIClient) - }) - - it("starts the impersonator without creating a load balancer", func() { - startInformersAndController() - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 2) - requireNodesListed(kubeAPIClient.Actions()[0]) - requireCASecretWasCreated(kubeAPIClient.Actions()[1]) - requireTLSServerIsRunningWithoutCerts() - requireCredentialIssuer(newPendingStrategy()) - requireSigningCertProviderIsEmpty() - }) - - it("returns an error when the impersonation TLS server fails to start", func() { - impersonatorFuncError = errors.New("impersonation server start error") - startInformersAndController() - r.EqualError(runControllerSync(), "impersonation server start error") - requireCredentialIssuer(newErrorStrategy("impersonation server start error")) - requireSigningCertProviderIsEmpty() - }) + it("starts the impersonator without creating a load balancer", func() { + startInformersAndController() + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 2) + requireNodesListed(kubeAPIClient.Actions()[0]) + requireCASecretWasCreated(kubeAPIClient.Actions()[1]) + requireTLSServerIsRunningWithoutCerts() + requireCredentialIssuer(newPendingStrategyWaitingForLB()) + requireSigningCertProviderIsEmpty() }) - when("a load balancer and a secret already exists", func() { - var caCrt []byte - it.Before(func() { - addImpersonatorConfigMapToTracker(configMapResourceName, "mode: enabled", kubeInformerClient) - addNodeWithRoleToTracker("worker", kubeAPIClient) - ca := newCA() - caSecret := newActualCASecret(ca, caSecretName) - caCrt = caSecret.Data["ca.crt"] - addSecretToTrackers(caSecret, kubeAPIClient, kubeInformerClient) - tlsSecret := newActualTLSSecret(ca, tlsSecretName, localhostIP) - addSecretToTrackers(tlsSecret, kubeAPIClient, kubeInformerClient) - addLoadBalancerServiceWithIngressToTracker(loadBalancerServiceName, []corev1.LoadBalancerIngress{{IP: localhostIP}}, kubeInformerClient) - addLoadBalancerServiceWithIngressToTracker(loadBalancerServiceName, []corev1.LoadBalancerIngress{{IP: localhostIP}}, kubeAPIClient) - }) - - it("starts the impersonator with the existing tls certs, does not start loadbalancer or make tls secret", func() { - startInformersAndController() - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 1) - requireNodesListed(kubeAPIClient.Actions()[0]) - requireTLSServerIsRunning(caCrt, testServerAddr(), nil) - requireCredentialIssuer(newSuccessStrategy(localhostIP, caCrt)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - }) - }) - - when("the configmap has a hostname specified for the endpoint", func() { - const fakeHostname = "fake.example.com" - it.Before(func() { - configMapYAML := fmt.Sprintf("{mode: enabled, endpoint: %s}", fakeHostname) - addImpersonatorConfigMapToTracker(configMapResourceName, configMapYAML, kubeInformerClient) - addNodeWithRoleToTracker("worker", kubeAPIClient) - }) - - it("starts the impersonator, generates a valid cert for the specified hostname", func() { - startInformersAndController() - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 3) - requireNodesListed(kubeAPIClient.Actions()[0]) - ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) - // Check that the server is running and that TLS certs that are being served are are for fakeHostname. - requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) - requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - }) - }) - - when("the configmap has a endpoint which is an IP address with a port", func() { - const fakeIPWithPort = "127.0.0.1:3000" - it.Before(func() { - configMapYAML := fmt.Sprintf("{mode: enabled, endpoint: %s}", fakeIPWithPort) - addImpersonatorConfigMapToTracker(configMapResourceName, configMapYAML, kubeInformerClient) - addNodeWithRoleToTracker("worker", kubeAPIClient) - }) - - it("starts the impersonator, generates a valid cert for the specified IP address", func() { - startInformersAndController() - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 3) - requireNodesListed(kubeAPIClient.Actions()[0]) - ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) - // Check that the server is running and that TLS certs that are being served are are for fakeIPWithPort. - requireTLSServerIsRunning(ca, fakeIPWithPort, map[string]string{fakeIPWithPort: testServerAddr()}) - requireCredentialIssuer(newSuccessStrategy(fakeIPWithPort, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - }) - }) - - when("the configmap has a endpoint which is a hostname with a port", func() { - const fakeHostnameWithPort = "fake.example.com:3000" - it.Before(func() { - configMapYAML := fmt.Sprintf("{mode: enabled, endpoint: %s}", fakeHostnameWithPort) - addImpersonatorConfigMapToTracker(configMapResourceName, configMapYAML, kubeInformerClient) - addNodeWithRoleToTracker("worker", kubeAPIClient) - }) - - it("starts the impersonator, generates a valid cert for the specified hostname", func() { - startInformersAndController() - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 3) - requireNodesListed(kubeAPIClient.Actions()[0]) - ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) - // Check that the server is running and that TLS certs that are being served are are for fakeHostnameWithPort. - requireTLSServerIsRunning(ca, fakeHostnameWithPort, map[string]string{fakeHostnameWithPort: testServerAddr()}) - requireCredentialIssuer(newSuccessStrategy(fakeHostnameWithPort, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - }) - }) - - when("switching the configmap from ip address endpoint to hostname endpoint and back to ip address", func() { - const fakeHostname = "fake.example.com" - const fakeIP = "127.0.0.42" - var hostnameYAML = fmt.Sprintf("{mode: enabled, endpoint: %s}", fakeHostname) - var ipAddressYAML = fmt.Sprintf("{mode: enabled, endpoint: %s}", fakeIP) - it.Before(func() { - addImpersonatorConfigMapToTracker(configMapResourceName, ipAddressYAML, kubeInformerClient) - addNodeWithRoleToTracker("worker", kubeAPIClient) - }) - - it("regenerates the cert for the hostname, then regenerates it for the IP again", func() { - startInformersAndController() - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 3) - requireNodesListed(kubeAPIClient.Actions()[0]) - ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) - // Check that the server is running and that TLS certs that are being served are are for fakeIP. - requireTLSServerIsRunning(ca, fakeIP, map[string]string{fakeIP + httpsPort: testServerAddr()}) - requireCredentialIssuer(newSuccessStrategy(fakeIP, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - - // Simulate the informer cache's background update from its watch. - addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[1], kubeInformers.Core().V1().Secrets()) - addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets()) - - // Switch the endpoint config to a hostname. - updateImpersonatorConfigMapInInformerAndWait(configMapResourceName, hostnameYAML, kubeInformers.Core().V1().ConfigMaps()) - - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 5) - requireTLSSecretWasDeleted(kubeAPIClient.Actions()[3]) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[4], ca) // reuses the old CA - // Check that the server is running and that TLS certs that are being served are are for fakeHostname. - requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) - requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - - // Simulate the informer cache's background update from its watch. - deleteSecretFromTracker(tlsSecretName, kubeInformerClient) - waitForObjectToBeDeletedFromInformer(tlsSecretName, kubeInformers.Core().V1().Secrets()) - addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[4], kubeInformers.Core().V1().Secrets()) - - // Switch the endpoint config back to an IP. - updateImpersonatorConfigMapInInformerAndWait(configMapResourceName, ipAddressYAML, kubeInformers.Core().V1().ConfigMaps()) - - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 7) - requireTLSSecretWasDeleted(kubeAPIClient.Actions()[5]) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[6], ca) // reuses the old CA again - // Check that the server is running and that TLS certs that are being served are are for fakeIP. - requireTLSServerIsRunning(ca, fakeIP, map[string]string{fakeIP + httpsPort: testServerAddr()}) - requireCredentialIssuer(newSuccessStrategy(fakeIP, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - }) - }) - - when("the TLS cert goes missing and needs to be recreated, e.g. when a user manually deleted it", func() { - const fakeHostname = "fake.example.com" - it.Before(func() { - configMapYAML := fmt.Sprintf("{mode: enabled, endpoint: %s}", fakeHostname) - addImpersonatorConfigMapToTracker(configMapResourceName, configMapYAML, kubeInformerClient) - addNodeWithRoleToTracker("worker", kubeAPIClient) - startInformersAndController() - }) - - it("uses the existing CA cert the make a new TLS cert", func() { - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 3) - requireNodesListed(kubeAPIClient.Actions()[0]) - ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) - // Check that the server is running and that TLS certs that are being served are are for fakeHostname. - requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) - requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - - // Simulate the informer cache's background update from its watch. - addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[1], kubeInformers.Core().V1().Secrets()) - - // Delete the TLS Secret that was just created from the Kube API server. Note that we never - // simulated it getting added to the informer cache, so we don't need to remove it from there. - deleteSecretFromTracker(tlsSecretName, kubeAPIClient) - - // Run again. It should create a new TLS cert using the old CA cert. - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 4) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[3], ca) - // Check that the server is running and that TLS certs that are being served are are for fakeHostname. - requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) - requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - }) - }) - - when("the CA cert goes missing and needs to be recreated, e.g. when a user manually deleted it", func() { - const fakeHostname = "fake.example.com" - it.Before(func() { - configMapYAML := fmt.Sprintf("{mode: enabled, endpoint: %s}", fakeHostname) - addImpersonatorConfigMapToTracker(configMapResourceName, configMapYAML, kubeInformerClient) - addNodeWithRoleToTracker("worker", kubeAPIClient) - startInformersAndController() - }) - - it("makes a new CA cert, deletes the old TLS cert, and makes a new TLS cert using the new CA", func() { - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 3) - requireNodesListed(kubeAPIClient.Actions()[0]) - ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) - // Check that the server is running and that TLS certs that are being served are are for fakeHostname. - requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) - requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - - // Simulate the informer cache's background update from its watch. - addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets()) - - // Delete the CA Secret that was just created from the Kube API server. Note that we never - // simulated it getting added to the informer cache, so we don't need to remove it from there. - deleteSecretFromTracker(caSecretName, kubeAPIClient) - - // Run again. It should create both a new CA cert and a new TLS cert using the new CA cert. - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 6) - ca = requireCASecretWasCreated(kubeAPIClient.Actions()[3]) - requireTLSSecretWasDeleted(kubeAPIClient.Actions()[4]) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[5], ca) // created using the new CA - // Check that the server is running and that TLS certs that are being served are are for fakeHostname. - requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) - requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - }) - }) - - when("the CA cert is overwritten by another valid CA cert", func() { - const fakeHostname = "fake.example.com" - var caCrt []byte - it.Before(func() { - configMapYAML := fmt.Sprintf("{mode: enabled, endpoint: %s}", fakeHostname) - addImpersonatorConfigMapToTracker(configMapResourceName, configMapYAML, kubeInformerClient) - addNodeWithRoleToTracker("worker", kubeAPIClient) - startInformersAndController() - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 3) - requireNodesListed(kubeAPIClient.Actions()[0]) - ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) - // Check that the server is running and that TLS certs that are being served are are for fakeHostname. - requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) - requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - - // Simulate the informer cache's background update from its watch. - addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets()) - - // Simulate someone updating the CA Secret out of band, e.g. when a human edits it with kubectl. - // Delete the CA Secret that was just created from the Kube API server. Note that we never - // simulated it getting added to the informer cache, so we don't need to remove it from there. - // Then add a new one. Delete + new = update, since only the final state is observed. - deleteSecretFromTracker(caSecretName, kubeAPIClient) - anotherCA := newCA() - newCASecret := newActualCASecret(anotherCA, caSecretName) - caCrt = newCASecret.Data["ca.crt"] - addSecretToTrackers(newCASecret, kubeAPIClient) - addObjectToInformerAndWait(newCASecret, kubeInformers.Core().V1().Secrets()) - }) - - it("deletes the old TLS cert and makes a new TLS cert using the new CA", func() { - // Run again. It should use the updated CA cert to create a new TLS cert. - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 5) - requireTLSSecretWasDeleted(kubeAPIClient.Actions()[3]) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[4], caCrt) // created using the updated CA - // Check that the server is running and that TLS certs that are being served are are for fakeHostname. - requireTLSServerIsRunning(caCrt, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) - requireCredentialIssuer(newSuccessStrategy(fakeHostname, caCrt)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - }) - - when("deleting the TLS cert due to mismatched CA results in an error", func() { - it.Before(func() { - kubeAPIClient.PrependReactor("delete", "secrets", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { - if action.(coretesting.DeleteAction).GetName() == tlsSecretName { - return true, nil, fmt.Errorf("error on tls secret delete") - } - return false, nil, nil - }) - }) - - it("returns an error", func() { - r.Error(runControllerSync(), "error on tls secret delete") - r.Len(kubeAPIClient.Actions(), 4) - requireTLSSecretWasDeleted(kubeAPIClient.Actions()[3]) // tried to delete cert but failed - requireCredentialIssuer(newErrorStrategy("error on tls secret delete")) - requireSigningCertProviderIsEmpty() - }) - }) + it("returns an error when the impersonation TLS server fails to start", func() { + impersonatorFuncError = errors.New("impersonation server start error") + startInformersAndController() + r.EqualError(runControllerSync(), "impersonation server start error") + requireCredentialIssuer(newErrorStrategy("impersonation server start error")) + requireSigningCertProviderIsEmpty() }) }) - when("the configuration switches from enabled to disabled mode", func() { + when("a clusterip already exists with ingress", func() { + const fakeIP = "127.0.0.123" it.Before(func() { - addImpersonatorConfigMapToTracker(configMapResourceName, "mode: enabled", kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeClusterIP, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + addClusterIPServiceToTracker(clusterIPServiceName, fakeIP, kubeInformerClient) + addClusterIPServiceToTracker(clusterIPServiceName, fakeIP, kubeAPIClient) + }) + + it("starts the impersonator without creating a clusterip", func() { + startInformersAndController() + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 3) + requireNodesListed(kubeAPIClient.Actions()[0]) + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) + requireTLSServerIsRunning(ca, fakeIP, map[string]string{fakeIP + ":443": testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeIP, ca)) + // requireSigningCertProviderHasLoadedCerts() + }) + }) + + when("a clusterip service exists with dual stack ips", func() { + const fakeIP1 = "127.0.0.123" + const fakeIP2 = "fd00::5118" + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeClusterIP, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + addDualStackClusterIPServiceToTracker(clusterIPServiceName, fakeIP1, fakeIP2, kubeInformerClient) + addDualStackClusterIPServiceToTracker(clusterIPServiceName, fakeIP1, fakeIP2, kubeAPIClient) + }) + + it("certs are valid for both ip addresses", func() { + startInformersAndController() + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 3) + requireNodesListed(kubeAPIClient.Actions()[0]) + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) + requireTLSServerIsRunning(ca, "["+fakeIP2+"]", map[string]string{"[fd00::5118]:443": testServerAddr()}) + requireTLSServerIsRunning(ca, fakeIP1, map[string]string{fakeIP1 + ":443": testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeIP1, ca)) + }) + }) + + when("a load balancer and a secret already exists", func() { + var caCrt []byte + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + ca := newCA() + caSecret := newActualCASecret(ca, caSecretName) + caCrt = caSecret.Data["ca.crt"] + addSecretToTrackers(caSecret, kubeAPIClient, kubeInformerClient) + tlsSecret := newActualTLSSecret(ca, tlsSecretName, localhostIP) + addSecretToTrackers(tlsSecret, kubeAPIClient, kubeInformerClient) + addLoadBalancerServiceWithIngressToTracker(loadBalancerServiceName, []corev1.LoadBalancerIngress{{IP: localhostIP}}, kubeInformerClient) + addLoadBalancerServiceWithIngressToTracker(loadBalancerServiceName, []corev1.LoadBalancerIngress{{IP: localhostIP}}, kubeAPIClient) + }) + + it("starts the impersonator with the existing tls certs, does not start loadbalancer or make tls secret", func() { + startInformersAndController() + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 1) + requireNodesListed(kubeAPIClient.Actions()[0]) + requireTLSServerIsRunning(caCrt, testServerAddr(), nil) + requireCredentialIssuer(newSuccessStrategy(localhostIP, caCrt)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + }) + + when("credentialissuer has service type loadbalancer and custom annotations", func() { + annotations := map[string]string{"some-annotation-key": "some-annotation-value"} + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeLoadBalancer, + Annotations: annotations, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + }) + + it("starts the impersonator, generates a valid cert for the specified hostname, starts a loadbalancer", func() { + startInformersAndController() + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 3) + requireNodesListed(kubeAPIClient.Actions()[0]) + lbService := requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) + require.Equal(t, lbService.Annotations, annotations) + requireCASecretWasCreated(kubeAPIClient.Actions()[2]) + requireTLSServerIsRunningWithoutCerts() + requireCredentialIssuer(newPendingStrategyWaitingForLB()) + requireSigningCertProviderIsEmpty() + }) + }) + + when("the CredentialIssuer has a hostname specified and service type none", func() { + const fakeHostname = "fake.example.com" + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: fakeHostname, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + }) + + it("starts the impersonator, generates a valid cert for the specified hostname", func() { + startInformersAndController() + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 3) + requireNodesListed(kubeAPIClient.Actions()[0]) + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) + // Check that the server is running and that TLS certs that are being served are are for fakeHostname. + requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + }) + + when("the CredentialIssuer has a hostname specified and service type loadbalancer", func() { + const fakeHostname = "fake.example.com" + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: fakeHostname, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeLoadBalancer, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + }) + + it("starts the impersonator, generates a valid cert for the specified hostname, starts a loadbalancer", func() { + startInformersAndController() + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 4) + requireNodesListed(kubeAPIClient.Actions()[0]) + requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[2]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[3], ca) + // Check that the server is running and that TLS certs that are being served are are for fakeHostname. + requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + }) + + when("the CredentialIssuer has a hostname specified and service type clusterip", func() { + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeClusterIP, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + }) + + it("starts the impersonator and creates a clusterip service", func() { + startInformersAndController() + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 3) + requireNodesListed(kubeAPIClient.Actions()[0]) + requireClusterIPWasCreated(kubeAPIClient.Actions()[1]) + requireCASecretWasCreated(kubeAPIClient.Actions()[2]) + // Check that the server is running without certs. + requireTLSServerIsRunningWithoutCerts() + requireCredentialIssuer(newPendingStrategyWaitingForLB()) + requireSigningCertProviderIsEmpty() + }) + }) + + when("the CredentialIssuer has a endpoint which is an IP address with a port", func() { + const fakeIPWithPort = "127.0.0.1:3000" + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: fakeIPWithPort, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + }) + + it("starts the impersonator, generates a valid cert for the specified IP address", func() { + startInformersAndController() + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 3) + requireNodesListed(kubeAPIClient.Actions()[0]) + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) + // Check that the server is running and that TLS certs that are being served are are for fakeIPWithPort. + requireTLSServerIsRunning(ca, fakeIPWithPort, map[string]string{fakeIPWithPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeIPWithPort, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + }) + + when("the CredentialIssuer has a endpoint which is a hostname with a port, service type none", func() { + const fakeHostnameWithPort = "fake.example.com:3000" + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: fakeHostnameWithPort, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + }) + + it("starts the impersonator, generates a valid cert for the specified hostname", func() { + startInformersAndController() + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 3) + requireNodesListed(kubeAPIClient.Actions()[0]) + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) + // Check that the server is running and that TLS certs that are being served are are for fakeHostnameWithPort. + requireTLSServerIsRunning(ca, fakeHostnameWithPort, map[string]string{fakeHostnameWithPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeHostnameWithPort, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + }) + + when("the CredentialIssuer has a endpoint which is a hostname with a port, service type loadbalancer with loadbalancerip", func() { + const fakeHostnameWithPort = "fake.example.com:3000" + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: fakeHostnameWithPort, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeLoadBalancer, + LoadBalancerIP: localhostIP, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + }) + + it("starts the impersonator, starts the loadbalancer, generates a valid cert for the specified hostname", func() { + startInformersAndController() + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 4) + requireNodesListed(kubeAPIClient.Actions()[0]) + lbService := requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) + require.Equal(t, lbService.Spec.LoadBalancerIP, localhostIP) + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[2]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[3], ca) + // Check that the server is running and that TLS certs that are being served are are for fakeHostnameWithPort. + requireTLSServerIsRunning(ca, fakeHostnameWithPort, map[string]string{fakeHostnameWithPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeHostnameWithPort, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + }) + + when("switching the CredentialIssuer from ip address endpoint to hostname endpoint and back to ip address", func() { + const fakeHostname = "fake.example.com" + const fakeIP = "127.0.0.42" + + var hostnameConfig = v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: fakeHostname, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + } + var ipAddressConfig = v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: fakeIP, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + } + + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: ipAddressConfig, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + }) + + it("regenerates the cert for the hostname, then regenerates it for the IP again", func() { + startInformersAndController() + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 3) + requireNodesListed(kubeAPIClient.Actions()[0]) + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) + // Check that the server is running and that TLS certs that are being served are are for fakeIP. + requireTLSServerIsRunning(ca, fakeIP, map[string]string{fakeIP + httpsPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeIP, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + + // Simulate the informer cache's background update from its watch. + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[1], kubeInformers.Core().V1().Secrets()) + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets()) + + // Switch the endpoint config to a hostname. + updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, hostnameConfig, pinnipedInformers.Config().V1alpha1().CredentialIssuers()) + + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 5) + requireTLSSecretWasDeleted(kubeAPIClient.Actions()[3]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[4], ca) // reuses the old CA + // Check that the server is running and that TLS certs that are being served are are for fakeHostname. + requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + + // Simulate the informer cache's background update from its watch. + deleteSecretFromTracker(tlsSecretName, kubeInformerClient) + waitForObjectToBeDeletedFromInformer(tlsSecretName, kubeInformers.Core().V1().Secrets()) + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[4], kubeInformers.Core().V1().Secrets()) + + // Switch the endpoint config back to an IP. + updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, ipAddressConfig, pinnipedInformers.Config().V1alpha1().CredentialIssuers()) + + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 7) + requireTLSSecretWasDeleted(kubeAPIClient.Actions()[5]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[6], ca) // reuses the old CA again + // Check that the server is running and that TLS certs that are being served are are for fakeIP. + requireTLSServerIsRunning(ca, fakeIP, map[string]string{fakeIP + httpsPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeIP, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + }) + + when("the TLS cert goes missing and needs to be recreated, e.g. when a user manually deleted it", func() { + const fakeHostname = "fake.example.com" + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: fakeHostname, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + startInformersAndController() + }) + + it("uses the existing CA cert the make a new TLS cert", func() { + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 3) + requireNodesListed(kubeAPIClient.Actions()[0]) + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) + // Check that the server is running and that TLS certs that are being served are are for fakeHostname. + requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + + // Simulate the informer cache's background update from its watch. + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[1], kubeInformers.Core().V1().Secrets()) + + // Delete the TLS Secret that was just created from the Kube API server. Note that we never + // simulated it getting added to the informer cache, so we don't need to remove it from there. + deleteSecretFromTracker(tlsSecretName, kubeAPIClient) + + // Run again. It should create a new TLS cert using the old CA cert. + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 4) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[3], ca) + // Check that the server is running and that TLS certs that are being served are are for fakeHostname. + requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + }) + + when("the CA cert goes missing and needs to be recreated, e.g. when a user manually deleted it", func() { + const fakeHostname = "fake.example.com" + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: fakeHostname, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + startInformersAndController() + }) + + it("makes a new CA cert, deletes the old TLS cert, and makes a new TLS cert using the new CA", func() { + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 3) + requireNodesListed(kubeAPIClient.Actions()[0]) + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) + // Check that the server is running and that TLS certs that are being served are are for fakeHostname. + requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + + // Simulate the informer cache's background update from its watch. + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets()) + + // Delete the CA Secret that was just created from the Kube API server. Note that we never + // simulated it getting added to the informer cache, so we don't need to remove it from there. + deleteSecretFromTracker(caSecretName, kubeAPIClient) + + // Run again. It should create both a new CA cert and a new TLS cert using the new CA cert. + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 6) + ca = requireCASecretWasCreated(kubeAPIClient.Actions()[3]) + requireTLSSecretWasDeleted(kubeAPIClient.Actions()[4]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[5], ca) // created using the new CA + // Check that the server is running and that TLS certs that are being served are are for fakeHostname. + requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + }) + + when("the CA cert is overwritten by another valid CA cert", func() { + const fakeHostname = "fake.example.com" + var caCrt []byte + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: fakeHostname, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + startInformersAndController() + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 3) + requireNodesListed(kubeAPIClient.Actions()[0]) + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) + // Check that the server is running and that TLS certs that are being served are are for fakeHostname. + requireTLSServerIsRunning(ca, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeHostname, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + + // Simulate the informer cache's background update from its watch. + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets()) + + // Simulate someone updating the CA Secret out of band, e.g. when a human edits it with kubectl. + // Delete the CA Secret that was just created from the Kube API server. Note that we never + // simulated it getting added to the informer cache, so we don't need to remove it from there. + // Then add a new one. Delete + new = update, since only the final state is observed. + deleteSecretFromTracker(caSecretName, kubeAPIClient) + anotherCA := newCA() + newCASecret := newActualCASecret(anotherCA, caSecretName) + caCrt = newCASecret.Data["ca.crt"] + addSecretToTrackers(newCASecret, kubeAPIClient) + addObjectToKubeInformerAndWait(newCASecret, kubeInformers.Core().V1().Secrets()) + }) + + it("deletes the old TLS cert and makes a new TLS cert using the new CA", func() { + // Run again. It should use the updated CA cert to create a new TLS cert. + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 5) + requireTLSSecretWasDeleted(kubeAPIClient.Actions()[3]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[4], caCrt) // created using the updated CA + // Check that the server is running and that TLS certs that are being served are are for fakeHostname. + requireTLSServerIsRunning(caCrt, fakeHostname, map[string]string{fakeHostname + httpsPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeHostname, caCrt)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + + when("deleting the TLS cert due to mismatched CA results in an error", func() { + it.Before(func() { + kubeAPIClient.PrependReactor("delete", "secrets", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { + if action.(coretesting.DeleteAction).GetName() == tlsSecretName { + return true, nil, fmt.Errorf("error on tls secret delete") + } + return false, nil, nil + }) + }) + + it("returns an error", func() { + r.Error(runControllerSync(), "error on tls secret delete") + r.Len(kubeAPIClient.Actions(), 4) + requireTLSSecretWasDeleted(kubeAPIClient.Actions()[3]) // tried to delete cert but failed + requireCredentialIssuer(newErrorStrategy("error on tls secret delete")) + requireSigningCertProviderIsEmpty() + }) + }) + }) + }) + + when("the configuration switches from enabled to disabled mode", func() { + when("service type loadbalancer", func() { + it.Before(func() { + addSecretToTrackers(signingCASecret, kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) addNodeWithRoleToTracker("worker", kubeAPIClient) }) @@ -1713,119 +2145,410 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { requireNodesListed(kubeAPIClient.Actions()[0]) requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) requireCASecretWasCreated(kubeAPIClient.Actions()[2]) - requireCredentialIssuer(newPendingStrategy()) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) requireSigningCertProviderIsEmpty() // Simulate the informer cache's background update from its watch. addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[1], kubeInformers.Core().V1().Services()) addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets()) - // Update the configmap. - updateImpersonatorConfigMapInInformerAndWait(configMapResourceName, "mode: disabled", kubeInformers.Core().V1().ConfigMaps()) + // Update the CredentialIssuer. + updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeDisabled, + }, + }, pinnipedInformers.Config().V1alpha1().CredentialIssuers()) r.NoError(runControllerSync()) requireTLSServerIsNoLongerRunning() r.Len(kubeAPIClient.Actions(), 4) - requireLoadBalancerWasDeleted(kubeAPIClient.Actions()[3]) + requireServiceWasDeleted(kubeAPIClient.Actions()[3], loadBalancerServiceName) requireCredentialIssuer(newManuallyDisabledStrategy()) requireSigningCertProviderIsEmpty() deleteServiceFromTracker(loadBalancerServiceName, kubeInformerClient) waitForObjectToBeDeletedFromInformer(loadBalancerServiceName, kubeInformers.Core().V1().Services()) - // Update the configmap again. - updateImpersonatorConfigMapInInformerAndWait(configMapResourceName, "mode: enabled", kubeInformers.Core().V1().ConfigMaps()) + // Update the CredentialIssuer again. + updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + }, + }, pinnipedInformers.Config().V1alpha1().CredentialIssuers()) r.NoError(runControllerSync()) requireTLSServerIsRunningWithoutCerts() r.Len(kubeAPIClient.Actions(), 5) requireLoadBalancerWasCreated(kubeAPIClient.Actions()[4]) - requireCredentialIssuer(newPendingStrategy()) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) requireSigningCertProviderIsEmpty() }) }) - when("the endpoint switches from specified, to not specified, to specified again", func() { + when("service type clusterip", func() { it.Before(func() { - configMapYAML := fmt.Sprintf("{mode: enabled, endpoint: %s}", localhostIP) - addImpersonatorConfigMapToTracker(configMapResourceName, configMapYAML, kubeInformerClient) + addSecretToTrackers(signingCASecret, kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeClusterIP, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) addNodeWithRoleToTracker("worker", kubeAPIClient) }) - it("doesn't create, then creates, then deletes the load balancer", func() { + it("starts the impersonator and clusterip, then shuts it down, then starts it again", func() { startInformersAndController() - // Should have started in "enabled" mode with an "endpoint", so no load balancer is needed. r.NoError(runControllerSync()) + requireTLSServerIsRunningWithoutCerts() r.Len(kubeAPIClient.Actions(), 3) requireNodesListed(kubeAPIClient.Actions()[0]) - ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) // created immediately because "endpoint" was specified - requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) - requireTLSServerIsRunning(ca, testServerAddr(), nil) - requireCredentialIssuer(newSuccessStrategy(localhostIP, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + requireClusterIPWasCreated(kubeAPIClient.Actions()[1]) + requireCASecretWasCreated(kubeAPIClient.Actions()[2]) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) + requireSigningCertProviderIsEmpty() // Simulate the informer cache's background update from its watch. - addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[1], kubeInformers.Core().V1().Secrets()) + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[1], kubeInformers.Core().V1().Services()) addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets()) - // Switch to "enabled" mode without an "endpoint", so a load balancer is needed now. - updateImpersonatorConfigMapInInformerAndWait(configMapResourceName, "mode: enabled", kubeInformers.Core().V1().ConfigMaps()) + // Update the CredentialIssuer. + updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeDisabled, + }, + }, pinnipedInformers.Config().V1alpha1().CredentialIssuers()) r.NoError(runControllerSync()) + requireTLSServerIsNoLongerRunning() + r.Len(kubeAPIClient.Actions(), 4) + requireServiceWasDeleted(kubeAPIClient.Actions()[3], clusterIPServiceName) + requireCredentialIssuer(newManuallyDisabledStrategy()) + requireSigningCertProviderIsEmpty() + + deleteServiceFromTracker(clusterIPServiceName, kubeInformerClient) + waitForObjectToBeDeletedFromInformer(clusterIPServiceName, kubeInformers.Core().V1().Services()) + + // Update the CredentialIssuer again. + updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeClusterIP, + }, + }, + }, pinnipedInformers.Config().V1alpha1().CredentialIssuers()) + + r.NoError(runControllerSync()) + requireTLSServerIsRunningWithoutCerts() r.Len(kubeAPIClient.Actions(), 5) - requireLoadBalancerWasCreated(kubeAPIClient.Actions()[3]) - requireTLSSecretWasDeleted(kubeAPIClient.Actions()[4]) // the Secret was deleted because it contained a cert with the wrong IP - requireTLSServerIsRunningWithoutCerts() - requireCredentialIssuer(newPendingStrategy()) + requireClusterIPWasCreated(kubeAPIClient.Actions()[4]) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) requireSigningCertProviderIsEmpty() - - // Simulate the informer cache's background update from its watch. - addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[3], kubeInformers.Core().V1().Services()) - deleteSecretFromTracker(tlsSecretName, kubeInformerClient) - waitForObjectToBeDeletedFromInformer(tlsSecretName, kubeInformers.Core().V1().Secrets()) - - // The controller should be waiting for the load balancer's ingress to become available. - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 5) // no new actions while it is waiting for the load balancer's ingress - requireTLSServerIsRunningWithoutCerts() - requireCredentialIssuer(newPendingStrategy()) - requireSigningCertProviderIsEmpty() - - // Update the ingress of the LB in the informer's client and run Sync again. - fakeIP := "127.0.0.123" - updateLoadBalancerServiceInInformerAndWait(loadBalancerServiceName, []corev1.LoadBalancerIngress{{IP: fakeIP}}, kubeInformers.Core().V1().Services()) - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 6) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[5], ca) // reuses the existing CA - // Check that the server is running and that TLS certs that are being served are are for fakeIP. - requireTLSServerIsRunning(ca, fakeIP, map[string]string{fakeIP + httpsPort: testServerAddr()}) - requireCredentialIssuer(newSuccessStrategy(fakeIP, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) - - // Simulate the informer cache's background update from its watch. - addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[5], kubeInformers.Core().V1().Secrets()) - - // Now switch back to having the "endpoint" specified, so the load balancer is not needed anymore. - configMapYAML := fmt.Sprintf("{mode: enabled, endpoint: %s}", localhostIP) - updateImpersonatorConfigMapInInformerAndWait(configMapResourceName, configMapYAML, kubeInformers.Core().V1().ConfigMaps()) - - r.NoError(runControllerSync()) - r.Len(kubeAPIClient.Actions(), 9) - requireLoadBalancerWasDeleted(kubeAPIClient.Actions()[6]) - requireTLSSecretWasDeleted(kubeAPIClient.Actions()[7]) - requireTLSSecretWasCreated(kubeAPIClient.Actions()[8], ca) // recreated because the endpoint was updated, reused the old CA - requireTLSServerIsRunning(ca, testServerAddr(), nil) - requireCredentialIssuer(newSuccessStrategy(localhostIP, ca)) - requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) }) }) }) + when("the endpoint and mode switch from specified with no service, to not specified, to specified again", func() { + it.Before(func() { + addSecretToTrackers(signingCASecret, kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: localhostIP, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + }) + + it("doesn't create, then creates, then deletes the load balancer", func() { + startInformersAndController() + + // Should have started in "enabled" mode with an "endpoint", so no load balancer is needed. + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 3) + requireNodesListed(kubeAPIClient.Actions()[0]) + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[1]) // created immediately because "endpoint" was specified + requireTLSSecretWasCreated(kubeAPIClient.Actions()[2], ca) + requireTLSServerIsRunning(ca, testServerAddr(), nil) + requireCredentialIssuer(newSuccessStrategy(localhostIP, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + + // Simulate the informer cache's background update from its watch. + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[1], kubeInformers.Core().V1().Secrets()) + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets()) + + // Switch to "enabled" mode without an "endpoint", so a load balancer is needed now. + updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + }, + }, pinnipedInformers.Config().V1alpha1().CredentialIssuers()) + + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 5) + requireLoadBalancerWasCreated(kubeAPIClient.Actions()[3]) + requireTLSSecretWasDeleted(kubeAPIClient.Actions()[4]) // the Secret was deleted because it contained a cert with the wrong IP + requireTLSServerIsRunningWithoutCerts() + requireCredentialIssuer(newPendingStrategyWaitingForLB()) + requireSigningCertProviderIsEmpty() + + // Simulate the informer cache's background update from its watch. + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[3], kubeInformers.Core().V1().Services()) + deleteSecretFromTracker(tlsSecretName, kubeInformerClient) + waitForObjectToBeDeletedFromInformer(tlsSecretName, kubeInformers.Core().V1().Secrets()) + + // The controller should be waiting for the load balancer's ingress to become available. + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 5) // no new actions while it is waiting for the load balancer's ingress + requireTLSServerIsRunningWithoutCerts() + requireCredentialIssuer(newPendingStrategyWaitingForLB()) + requireSigningCertProviderIsEmpty() + + // Update the ingress of the LB in the informer's client and run Sync again. + fakeIP := "127.0.0.123" + updateLoadBalancerServiceInInformerAndWait(loadBalancerServiceName, []corev1.LoadBalancerIngress{{IP: fakeIP}}, kubeInformers.Core().V1().Services()) + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 6) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[5], ca) // reuses the existing CA + // Check that the server is running and that TLS certs that are being served are are for fakeIP. + requireTLSServerIsRunning(ca, fakeIP, map[string]string{fakeIP + httpsPort: testServerAddr()}) + requireCredentialIssuer(newSuccessStrategy(fakeIP, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + + // Simulate the informer cache's background update from its watch. + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[5], kubeInformers.Core().V1().Secrets()) + + // Now switch back to having the "endpoint" specified and explicitly saying that we don't want the load balancer service. + updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: localhostIP, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, pinnipedInformers.Config().V1alpha1().CredentialIssuers()) + + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 9) + requireServiceWasDeleted(kubeAPIClient.Actions()[6], loadBalancerServiceName) + requireTLSSecretWasDeleted(kubeAPIClient.Actions()[7]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[8], ca) // recreated because the endpoint was updated, reused the old CA + requireTLSServerIsRunning(ca, testServerAddr(), nil) + requireCredentialIssuer(newSuccessStrategy(localhostIP, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + }) + + when("requesting a load balancer via CredentialIssuer, then updating the annotations", func() { + it.Before(func() { + addSecretToTrackers(signingCASecret, kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: localhostIP, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeLoadBalancer, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + }) + + it("creates the load balancer without annotations, then adds them", func() { + startInformersAndController() + + // Should have started in "enabled" mode with service type load balancer, so one is created. + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 4) + requireNodesListed(kubeAPIClient.Actions()[0]) + lbService := requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) + require.Equal(t, map[string]string(nil), lbService.Annotations) // there should be no annotations at first + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[2]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[3], ca) + requireTLSServerIsRunning(ca, testServerAddr(), nil) + requireCredentialIssuer(newSuccessStrategy(localhostIP, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + + // Simulate the informer cache's background update from its watch. + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[1], kubeInformers.Core().V1().Services()) + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets()) + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[3], kubeInformers.Core().V1().Secrets()) + + // Add annotations to the spec. + annotations := map[string]string{"my-annotation-key": "my-annotation-val"} + updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: localhostIP, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeLoadBalancer, + Annotations: annotations, + }, + }, + }, pinnipedInformers.Config().V1alpha1().CredentialIssuers()) + + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 5) // one more item to update the loadbalancer + lbService = requireLoadBalancerWasUpdated(kubeAPIClient.Actions()[4]) + require.Equal(t, annotations, lbService.Annotations) // now the annotations should exist on the load balancer + requireTLSServerIsRunning(ca, testServerAddr(), nil) + requireCredentialIssuer(newSuccessStrategy(localhostIP, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + }) + + when("requesting a cluster ip via CredentialIssuer, then updating the annotations", func() { + it.Before(func() { + addSecretToTrackers(signingCASecret, kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: localhostIP, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeClusterIP, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + }) + + it("creates the cluster ip without annotations, then adds them", func() { + startInformersAndController() + + // Should have started in "enabled" mode with service type load balancer, so one is created. + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 4) + requireNodesListed(kubeAPIClient.Actions()[0]) + clusterIPService := requireClusterIPWasCreated(kubeAPIClient.Actions()[1]) + require.Equal(t, map[string]string(nil), clusterIPService.Annotations) // there should be no annotations at first + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[2]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[3], ca) + requireTLSServerIsRunning(ca, testServerAddr(), nil) + requireCredentialIssuer(newSuccessStrategy(localhostIP, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + + // Simulate the informer cache's background update from its watch. + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[1], kubeInformers.Core().V1().Services()) + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets()) + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[3], kubeInformers.Core().V1().Secrets()) + + // Add annotations to the spec. + annotations := map[string]string{"my-annotation-key": "my-annotation-val"} + updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: localhostIP, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeClusterIP, + Annotations: annotations, + }, + }, + }, pinnipedInformers.Config().V1alpha1().CredentialIssuers()) + + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 5) // one more item to update the loadbalancer + clusterIPService = requireClusterIPWasUpdated(kubeAPIClient.Actions()[4]) + require.Equal(t, annotations, clusterIPService.Annotations) // now the annotations should exist on the load balancer + requireTLSServerIsRunning(ca, testServerAddr(), nil) + requireCredentialIssuer(newSuccessStrategy(localhostIP, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + }) + + when("requesting a load balancer via CredentialIssuer, then adding a static loadBalancerIP to the spec", func() { + it.Before(func() { + addSecretToTrackers(signingCASecret, kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: localhostIP, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeLoadBalancer, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("worker", kubeAPIClient) + }) + + it("creates the load balancer without loadBalancerIP set, then adds it", func() { + startInformersAndController() + + // Should have started in "enabled" mode with service type load balancer, so one is created. + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 4) + requireNodesListed(kubeAPIClient.Actions()[0]) + lbService := requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) + require.Equal(t, map[string]string(nil), lbService.Annotations) // there should be no annotations at first + require.Equal(t, "", lbService.Spec.LoadBalancerIP) + ca := requireCASecretWasCreated(kubeAPIClient.Actions()[2]) + requireTLSSecretWasCreated(kubeAPIClient.Actions()[3], ca) + requireTLSServerIsRunning(ca, testServerAddr(), nil) + requireCredentialIssuer(newSuccessStrategy(localhostIP, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + + // Simulate the informer cache's background update from its watch. + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[1], kubeInformers.Core().V1().Services()) + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[2], kubeInformers.Core().V1().Secrets()) + addObjectFromCreateActionToInformerAndWait(kubeAPIClient.Actions()[3], kubeInformers.Core().V1().Secrets()) + + // Add annotations to the spec. + loadBalancerIP := "1.2.3.4" + updateCredentialIssuerInInformerAndWait(credentialIssuerResourceName, v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: localhostIP, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeLoadBalancer, + LoadBalancerIP: loadBalancerIP, + }, + }, + }, pinnipedInformers.Config().V1alpha1().CredentialIssuers()) + + r.NoError(runControllerSync()) + r.Len(kubeAPIClient.Actions(), 5) // one more item to update the loadbalancer + lbService = requireLoadBalancerWasUpdated(kubeAPIClient.Actions()[4]) + require.Equal(t, loadBalancerIP, lbService.Spec.LoadBalancerIP) + requireTLSServerIsRunning(ca, testServerAddr(), nil) + requireCredentialIssuer(newSuccessStrategy(localhostIP, ca)) + requireSigningCertProviderHasLoadedCerts(signingCACertPEM, signingCAKeyPEM) + }) + }) + when("sync is called more than once", func() { it.Before(func() { addSecretToTrackers(signingCASecret, kubeInformerClient) addNodeWithRoleToTracker("worker", kubeAPIClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeAuto, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) }) it("only starts the impersonator once and only lists the cluster's nodes once", func() { @@ -1836,7 +2559,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) requireCASecretWasCreated(kubeAPIClient.Actions()[2]) requireTLSServerIsRunningWithoutCerts() - requireCredentialIssuer(newPendingStrategy()) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) requireSigningCertProviderIsEmpty() // Simulate the informer cache's background update from its watch. @@ -1846,7 +2569,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { r.NoError(runControllerSync()) r.Equal(1, impersonatorFuncWasCalled) // wasn't started a second time requireTLSServerIsRunningWithoutCerts() // still running - requireCredentialIssuer(newPendingStrategy()) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) requireSigningCertProviderIsEmpty() r.Len(kubeAPIClient.Actions(), 3) // no new API calls }) @@ -1859,7 +2582,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) ca := requireCASecretWasCreated(kubeAPIClient.Actions()[2]) requireTLSServerIsRunningWithoutCerts() - requireCredentialIssuer(newPendingStrategy()) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) requireSigningCertProviderIsEmpty() // Simulate the informer cache's background update from its watch. @@ -1896,7 +2619,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) ca := requireCASecretWasCreated(kubeAPIClient.Actions()[2]) requireTLSServerIsRunningWithoutCerts() - requireCredentialIssuer(newPendingStrategy()) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) requireSigningCertProviderIsEmpty() // Simulate the informer cache's background update from its watch. @@ -1939,10 +2662,19 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { it.Before(func() { addSecretToTrackers(signingCASecret, kubeInformerClient) - r.NoError(pinnipedAPIClient.Tracker().Add(&v1alpha1.CredentialIssuer{ + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, - Status: v1alpha1.CredentialIssuerStatus{Strategies: []v1alpha1.CredentialIssuerStrategy{preExistingStrategy}}, - })) + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeAuto, + }, + }, + Status: v1alpha1.CredentialIssuerStatus{ + Strategies: []v1alpha1.CredentialIssuerStrategy{ + preExistingStrategy, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) addNodeWithRoleToTracker("worker", kubeAPIClient) }) @@ -1955,12 +2687,20 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) requireCASecretWasCreated(kubeAPIClient.Actions()[2]) credentialIssuer := getCredentialIssuer() - r.Equal([]v1alpha1.CredentialIssuerStrategy{preExistingStrategy, newPendingStrategy()}, credentialIssuer.Status.Strategies) + r.Equal([]v1alpha1.CredentialIssuerStrategy{preExistingStrategy, newPendingStrategyWaitingForLB()}, credentialIssuer.Status.Strategies) }) }) when("getting the control plane nodes returns an error, e.g. when there are no nodes", func() { it("returns an error", func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeAuto, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) startInformersAndController() r.EqualError(runControllerSync(), "no nodes found") requireCredentialIssuer(newErrorStrategy("no nodes found")) @@ -1973,6 +2713,14 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { it.Before(func() { addNodeWithRoleToTracker("worker", kubeAPIClient) impersonatorFuncReturnedFuncError = errors.New("some immediate impersonator startup error") + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeAuto, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) }) it("causes an immediate resync, returns an error on that next sync, and then restarts the server in a following sync", func() { @@ -1992,7 +2740,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { requireNodesListed(kubeAPIClient.Actions()[0]) requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) requireCASecretWasCreated(kubeAPIClient.Actions()[2]) - requireCredentialIssuer(newPendingStrategy()) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) requireSigningCertProviderIsEmpty() // Simulate the informer cache's background update from its watch. @@ -2020,7 +2768,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { // Now everything should be working correctly. r.NoError(runControllerSync()) requireTLSServerIsRunningWithoutCerts() - requireCredentialIssuer(newPendingStrategy()) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) requireSigningCertProviderIsEmpty() }) }) @@ -2028,6 +2776,14 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { when("the impersonator server dies for no apparent reason after running for a while", func() { it.Before(func() { addNodeWithRoleToTracker("worker", kubeAPIClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeAuto, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) }) it("causes an immediate resync, returns an error on that next sync, and then restarts the server in a following sync", func() { @@ -2040,7 +2796,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { requireNodesListed(kubeAPIClient.Actions()[0]) requireLoadBalancerWasCreated(kubeAPIClient.Actions()[1]) requireCASecretWasCreated(kubeAPIClient.Actions()[2]) - requireCredentialIssuer(newPendingStrategy()) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) requireSigningCertProviderIsEmpty() requireTLSServerIsRunningWithoutCerts() @@ -2072,19 +2828,119 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { // Now everything should be working correctly. r.NoError(runControllerSync()) requireTLSServerIsRunningWithoutCerts() - requireCredentialIssuer(newPendingStrategy()) + requireCredentialIssuer(newPendingStrategyWaitingForLB()) requireSigningCertProviderIsEmpty() }) }) - when("the configmap is invalid", func() { + when("the CredentialIssuer has nil impersonation spec", func() { it.Before(func() { - addImpersonatorConfigMapToTracker(configMapResourceName, "not yaml", kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: nil, + }, + }, pinnipedInformerClient, pinnipedAPIClient) }) it("returns an error", func() { startInformersAndController() - errString := "invalid impersonator configuration: decode yaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type impersonator.Config" + errString := `could not load CredentialIssuer: spec.impersonationProxy is nil` + r.EqualError(runControllerSync(), errString) + requireCredentialIssuer(newErrorStrategy(errString)) + requireSigningCertProviderIsEmpty() + requireTLSServerWasNeverStarted() + }) + }) + + when("the CredentialIssuer has invalid mode", func() { + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: "not-valid", + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + }) + + it("returns an error", func() { + startInformersAndController() + errString := `could not load CredentialIssuer spec.impersonationProxy: invalid proxy mode "not-valid" (expected auto, disabled, or enabled)` + r.EqualError(runControllerSync(), errString) + requireCredentialIssuer(newErrorStrategy(errString)) + requireSigningCertProviderIsEmpty() + requireTLSServerWasNeverStarted() + }) + }) + + when("the CredentialIssuer has invalid service type", func() { + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: "not-valid", + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + }) + + it("returns an error", func() { + startInformersAndController() + errString := `could not load CredentialIssuer spec.impersonationProxy: invalid service type "not-valid" (expected None, LoadBalancer, or ClusterIP)` + r.EqualError(runControllerSync(), errString) + requireCredentialIssuer(newErrorStrategy(errString)) + requireSigningCertProviderIsEmpty() + requireTLSServerWasNeverStarted() + }) + }) + + when("the CredentialIssuer has invalid LoadBalancerIP", func() { + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + LoadBalancerIP: "invalid-ip-address", + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + }) + + it("returns an error", func() { + startInformersAndController() + errString := `could not load CredentialIssuer spec.impersonationProxy: invalid LoadBalancerIP "invalid-ip-address"` + r.EqualError(runControllerSync(), errString) + requireCredentialIssuer(newErrorStrategy(errString)) + requireSigningCertProviderIsEmpty() + requireTLSServerWasNeverStarted() + }) + }) + + when("the CredentialIssuer has invalid ExternalEndpoint", func() { + it.Before(func() { + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: "[invalid", + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + }) + + it("returns an error", func() { + startInformersAndController() + errString := `could not load CredentialIssuer spec.impersonationProxy: invalid ExternalEndpoint "[invalid": address [invalid:443: missing ']' in address` r.EqualError(runControllerSync(), errString) requireCredentialIssuer(newErrorStrategy(errString)) requireSigningCertProviderIsEmpty() @@ -2093,11 +2949,76 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { }) when("there is an error creating the load balancer", func() { + it.Before(func() { + addNodeWithRoleToTracker("worker", kubeAPIClient) + kubeAPIClient.PrependReactor("create", "services", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, k8serrors.NewAlreadyExists( + action.GetResource().GroupResource(), + action.(coretesting.CreateAction).GetObject().(*corev1.Service).Name, + ) + }) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeAuto, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + }) + + it("returns an error", func() { + startInformersAndController() + r.EqualError(runControllerSync(), `services "some-service-resource-name" already exists`) + requireCredentialIssuer(newPendingStrategy(`services "some-service-resource-name" already exists`)) + requireSigningCertProviderIsEmpty() + requireTLSServerIsRunningWithoutCerts() + }) + }) + + when("there is an error deleting the load balancer", func() { + it.Before(func() { + addNodeWithRoleToTracker("worker", kubeAPIClient) + kubeAPIClient.PrependReactor("delete", "services", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, fmt.Errorf("error on delete") + }) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeDisabled, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addLoadBalancerServiceToTracker(loadBalancerServiceName, kubeAPIClient) + addLoadBalancerServiceToTracker(loadBalancerServiceName, kubeInformerClient) + }) + + it("returns an error", func() { + startInformersAndController() + r.EqualError(runControllerSync(), "error on delete") + requireCredentialIssuer(newErrorStrategy("error on delete")) + requireSigningCertProviderIsEmpty() + }) + }) + + when("there is an error creating the cluster ip", func() { it.Before(func() { addNodeWithRoleToTracker("worker", kubeAPIClient) kubeAPIClient.PrependReactor("create", "services", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { return true, nil, fmt.Errorf("error on create") }) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeAuto, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeClusterIP, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) }) it("returns an error", func() { @@ -2109,9 +3030,77 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { }) }) + when("there is an error updating the cluster ip", func() { + it.Before(func() { + addNodeWithRoleToTracker("worker", kubeAPIClient) + kubeAPIClient.PrependReactor("update", "services", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, fmt.Errorf("error on update") + }) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeAuto, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeClusterIP, + Annotations: map[string]string{"key": "val"}, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addClusterIPServiceToTracker(clusterIPServiceName, localhostIP, kubeAPIClient) + addClusterIPServiceToTracker(clusterIPServiceName, localhostIP, kubeInformerClient) + }) + + it("returns an error", func() { + startInformersAndController() + r.EqualError(runControllerSync(), "error on update") + requireCredentialIssuer(newErrorStrategy("error on update")) + requireSigningCertProviderIsEmpty() + requireTLSServerIsRunningWithoutCerts() + }) + }) + + when("there is an error deleting the cluster ip", func() { + it.Before(func() { + addNodeWithRoleToTracker("worker", kubeAPIClient) + kubeAPIClient.PrependReactor("delete", "services", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, fmt.Errorf("error on delete") + }) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeDisabled, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addClusterIPServiceToTracker(clusterIPServiceName, localhostIP, kubeAPIClient) + addClusterIPServiceToTracker(clusterIPServiceName, localhostIP, kubeInformerClient) + }) + + it("returns an error", func() { + startInformersAndController() + r.EqualError(runControllerSync(), "error on delete") + requireCredentialIssuer(newErrorStrategy("error on delete")) + requireSigningCertProviderIsEmpty() + }) + }) + when("there is an error creating the tls secret", func() { it.Before(func() { - addImpersonatorConfigMapToTracker(configMapResourceName, "{mode: enabled, endpoint: example.com}", kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: "example.com", + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) addNodeWithRoleToTracker("control-plane", kubeAPIClient) kubeAPIClient.PrependReactor("create", "secrets", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { createdSecret := action.(coretesting.CreateAction).GetObject().(*corev1.Secret) @@ -2137,7 +3126,18 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { when("there is an error creating the CA secret", func() { it.Before(func() { - addImpersonatorConfigMapToTracker(configMapResourceName, "{mode: enabled, endpoint: example.com}", kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: "example.com", + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) addNodeWithRoleToTracker("control-plane", kubeAPIClient) kubeAPIClient.PrependReactor("create", "secrets", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { createdSecret := action.(coretesting.CreateAction).GetObject().(*corev1.Secret) @@ -2163,7 +3163,18 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { when("the CA secret exists but is invalid while the TLS secret needs to be created", func() { it.Before(func() { addNodeWithRoleToTracker("control-plane", kubeAPIClient) - addImpersonatorConfigMapToTracker(configMapResourceName, "{mode: enabled, endpoint: example.com}", kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: "example.com", + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) addSecretToTrackers(newEmptySecret(caSecretName), kubeAPIClient, kubeInformerClient) }) @@ -2185,6 +3196,14 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { addLoadBalancerServiceToTracker(loadBalancerServiceName, kubeInformerClient) addLoadBalancerServiceToTracker(loadBalancerServiceName, kubeAPIClient) addSecretToTrackers(newEmptySecret(tlsSecretName), kubeAPIClient, kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeAuto, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) startInformersAndController() kubeAPIClient.PrependReactor("delete", "secrets", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { return true, nil, fmt.Errorf("error on delete") @@ -2198,7 +3217,7 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { requireTLSServerWasNeverStarted() r.Len(kubeAPIClient.Actions(), 3) requireNodesListed(kubeAPIClient.Actions()[0]) - requireLoadBalancerWasDeleted(kubeAPIClient.Actions()[1]) + requireServiceWasDeleted(kubeAPIClient.Actions()[1], loadBalancerServiceName) requireTLSSecretWasDeleted(kubeAPIClient.Actions()[2]) }) }) @@ -2207,7 +3226,14 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { it.Before(func() { addNodeWithRoleToTracker("control-plane", kubeAPIClient) addSecretToTrackers(newEmptySecret(tlsSecretName), kubeInformerClient) - addImpersonatorConfigMapToTracker(configMapResourceName, "{mode: disabled}", kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeDisabled, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) }) it("does not pass the not found error through", func() { @@ -2225,8 +3251,18 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { when("the PEM formatted data in the TLS Secret is not a valid cert", func() { it.Before(func() { addSecretToTrackers(signingCASecret, kubeInformerClient) - configMapYAML := fmt.Sprintf("{mode: enabled, endpoint: %s}", localhostIP) - addImpersonatorConfigMapToTracker(configMapResourceName, configMapYAML, kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: localhostIP, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) addNodeWithRoleToTracker("worker", kubeAPIClient) tlsSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -2281,7 +3317,14 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { var caCrt []byte it.Before(func() { addSecretToTrackers(signingCASecret, kubeInformerClient) - addImpersonatorConfigMapToTracker(configMapResourceName, "mode: enabled", kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) addNodeWithRoleToTracker("worker", kubeAPIClient) ca := newCA() caSecret := newActualCASecret(ca, caSecretName) @@ -2330,7 +3373,6 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { var caCrt []byte it.Before(func() { addSecretToTrackers(signingCASecret, kubeInformerClient) - addImpersonatorConfigMapToTracker(configMapResourceName, "mode: enabled", kubeInformerClient) addNodeWithRoleToTracker("worker", kubeAPIClient) ca := newCA() caSecret := newActualCASecret(ca, caSecretName) @@ -2341,6 +3383,14 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { addSecretToTrackers(tlsSecret, kubeAPIClient, kubeInformerClient) addLoadBalancerServiceWithIngressToTracker(loadBalancerServiceName, []corev1.LoadBalancerIngress{{IP: localhostIP}}, kubeInformerClient) addLoadBalancerServiceWithIngressToTracker(loadBalancerServiceName, []corev1.LoadBalancerIngress{{IP: localhostIP}}, kubeAPIClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeAuto, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) }) it("deletes the invalid certs, creates new certs, and starts the impersonator", func() { @@ -2381,14 +3431,22 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { it.Before(func() { addSecretToTrackers(signingCASecret, kubeInformerClient) addNodeWithRoleToTracker("worker", kubeAPIClient) - pinnipedAPIClient.PrependReactor("create", "credentialissuers", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { - return true, nil, fmt.Errorf("error on create") + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeAuto, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + pinnipedAPIClient.PrependReactor("update", "credentialissuers", func(action coretesting.Action) (handled bool, ret runtime.Object, err error) { + return true, nil, fmt.Errorf("error on update") }) }) it("returns the error", func() { startInformersAndController() - r.EqualError(runControllerSync(), "could not create or update credentialissuer: create failed: error on create") + r.EqualError(runControllerSync(), "failed to update CredentialIssuer status: error on update") }) when("there is also a more fundamental error while starting the impersonator", func() { @@ -2398,9 +3456,9 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { }) }) - it("returns the more fundamental error instead of the CredentialIssuer error", func() { + it("returns both errors", func() { startInformersAndController() - r.EqualError(runControllerSync(), "error on service creation") + r.EqualError(runControllerSync(), "[error on service creation, failed to update CredentialIssuer status: error on update]") }) }) }) @@ -2408,8 +3466,18 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { when("the impersonator is ready but there is a problem with the signing secret, which should be created by another controller", func() { const fakeHostname = "foo.example.com" it.Before(func() { - configMapYAML := fmt.Sprintf("{mode: enabled, endpoint: %s}", fakeHostname) - addImpersonatorConfigMapToTracker(configMapResourceName, configMapYAML, kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: fakeHostname, + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) addNodeWithRoleToTracker("worker", kubeAPIClient) }) @@ -2488,6 +3556,33 @@ func TestImpersonatorConfigControllerSync(t *testing.T) { }) }) }) + + when("CredentialIssuer spec validation", func() { + when("the impersonator is enabled but the service type is none and the external endpoint is empty", func() { + it.Before(func() { + addSecretToTrackers(signingCASecret, kubeInformerClient) + addCredentialIssuerToTrackers(v1alpha1.CredentialIssuer{ + ObjectMeta: metav1.ObjectMeta{Name: credentialIssuerResourceName}, + Spec: v1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &v1alpha1.ImpersonationProxySpec{ + Mode: v1alpha1.ImpersonationProxyModeEnabled, + ExternalEndpoint: "", + Service: v1alpha1.ImpersonationProxyServiceSpec{ + Type: v1alpha1.ImpersonationProxyServiceTypeNone, + }, + }, + }, + }, pinnipedInformerClient, pinnipedAPIClient) + addNodeWithRoleToTracker("control-plane", kubeAPIClient) + }) + + it("returns a validation error", func() { + startInformersAndController() + r.EqualError(runControllerSync(), "could not load CredentialIssuer spec.impersonationProxy: externalEndpoint must be set when service.type is None") + r.Len(kubeAPIClient.Actions(), 0) + }) + }) + }) }, spec.Parallel(), spec.Report(report.Terminal{})) } diff --git a/internal/controller/issuerconfig/create_or_update_credential_issuer_config.go b/internal/controller/issuerconfig/create_or_update_credential_issuer_config.go deleted file mode 100644 index 7d936b3a..00000000 --- a/internal/controller/issuerconfig/create_or_update_credential_issuer_config.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package issuerconfig - -import ( - "context" - "fmt" - - "k8s.io/apimachinery/pkg/api/equality" - - k8serrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/util/retry" - - configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" - pinnipedclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" -) - -func CreateOrUpdateCredentialIssuerStatus( - ctx context.Context, - credentialIssuerResourceName string, - credentialIssuerLabels map[string]string, - pinnipedClient pinnipedclientset.Interface, - applyUpdatesToCredentialIssuerFunc func(configToUpdate *configv1alpha1.CredentialIssuerStatus), -) error { - err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - existingCredentialIssuer, err := pinnipedClient. - ConfigV1alpha1(). - CredentialIssuers(). - Get(ctx, credentialIssuerResourceName, metav1.GetOptions{}) - - notFound := k8serrors.IsNotFound(err) - if err != nil && !notFound { - return fmt.Errorf("get failed: %w", err) - } - - credentialIssuersClient := pinnipedClient.ConfigV1alpha1().CredentialIssuers() - - if notFound { - // create an empty credential issuer - minCredentialIssuer := minimalValidCredentialIssuer(credentialIssuerResourceName, credentialIssuerLabels) - - newCredentialIssuer, err := credentialIssuersClient.Create(ctx, minCredentialIssuer, metav1.CreateOptions{}) - if err != nil { - return fmt.Errorf("create failed: %w", err) - } - - existingCredentialIssuer = newCredentialIssuer - } - - // check to see if we need to update the status - credentialIssuer := existingCredentialIssuer.DeepCopy() - applyUpdatesToCredentialIssuerFunc(&credentialIssuer.Status) - - if equality.Semantic.DeepEqual(existingCredentialIssuer, credentialIssuer) { - // Nothing interesting would change as a result of this update, so skip it - return nil - } - - if _, err := credentialIssuersClient.UpdateStatus(ctx, credentialIssuer, metav1.UpdateOptions{}); err != nil { - return err - } - - return nil - }) - - if err != nil { - return fmt.Errorf("could not create or update credentialissuer: %w", err) - } - return nil -} - -func minimalValidCredentialIssuer( - credentialIssuerName string, - credentialIssuerLabels map[string]string, -) *configv1alpha1.CredentialIssuer { - return &configv1alpha1.CredentialIssuer{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{ - Name: credentialIssuerName, - Labels: credentialIssuerLabels, - }, - } -} diff --git a/internal/controller/issuerconfig/create_or_update_credential_issuer_config_test.go b/internal/controller/issuerconfig/create_or_update_credential_issuer_config_test.go deleted file mode 100644 index beb20e56..00000000 --- a/internal/controller/issuerconfig/create_or_update_credential_issuer_config_test.go +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package issuerconfig - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/sclevine/spec" - "github.com/sclevine/spec/report" - "github.com/stretchr/testify/require" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - coretesting "k8s.io/client-go/testing" - apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" - - configv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" - pinnipedfake "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake" -) - -func TestCreateOrUpdateCredentialIssuerStatus(t *testing.T) { - spec.Run(t, "specs", func(t *testing.T, when spec.G, it spec.S) { - var r *require.Assertions - var ctx context.Context - var pinnipedAPIClient *pinnipedfake.Clientset - var credentialIssuerGVR schema.GroupVersionResource - const credentialIssuerResourceName = "some-resource-name" - - it.Before(func() { - r = require.New(t) - ctx = context.Background() - pinnipedAPIClient = pinnipedfake.NewSimpleClientset() - credentialIssuerGVR = schema.GroupVersionResource{ - Group: configv1alpha1.GroupName, - Version: configv1alpha1.SchemeGroupVersion.Version, - Resource: "credentialissuers", - } - }) - - when("the config does not exist", func() { - it("creates a new config and then updates it with the func parameter", func() { - err := CreateOrUpdateCredentialIssuerStatus( - ctx, - credentialIssuerResourceName, - map[string]string{ - "myLabelKey1": "myLabelValue1", - "myLabelKey2": "myLabelValue2", - }, - pinnipedAPIClient, - func(configToUpdate *configv1alpha1.CredentialIssuerStatus) { - configToUpdate.KubeConfigInfo = &configv1alpha1.CredentialIssuerKubeConfigInfo{ - CertificateAuthorityData: "some-ca-value", - } - }, - ) - r.NoError(err) - - expectedGetAction := coretesting.NewRootGetAction(credentialIssuerGVR, credentialIssuerResourceName) - - expectedCreateAction := coretesting.NewRootCreateAction( - credentialIssuerGVR, - &configv1alpha1.CredentialIssuer{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{ - Name: credentialIssuerResourceName, - Labels: map[string]string{ - "myLabelKey1": "myLabelValue1", - "myLabelKey2": "myLabelValue2", - }, - }, - }, - ) - - expectedUpdateAction := coretesting.NewRootUpdateSubresourceAction(credentialIssuerGVR, "status", - &configv1alpha1.CredentialIssuer{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{ - Name: credentialIssuerResourceName, - Labels: map[string]string{ - "myLabelKey1": "myLabelValue1", - "myLabelKey2": "myLabelValue2", - }, - }, - Status: configv1alpha1.CredentialIssuerStatus{ - KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ - Server: "", - CertificateAuthorityData: "some-ca-value", - }, - }, - }, - ) - - r.Equal([]coretesting.Action{expectedGetAction, expectedCreateAction, expectedUpdateAction}, pinnipedAPIClient.Actions()) - }) - - when("there is an unexpected error while creating the existing object", func() { - it.Before(func() { - pinnipedAPIClient.PrependReactor("create", "credentialissuers", func(_ coretesting.Action) (bool, runtime.Object, error) { - return true, nil, fmt.Errorf("error on create") - }) - }) - - it("returns an error", func() { - err := CreateOrUpdateCredentialIssuerStatus( - ctx, - credentialIssuerResourceName, - map[string]string{}, - pinnipedAPIClient, - func(configToUpdate *configv1alpha1.CredentialIssuerStatus) {}, - ) - r.EqualError(err, "could not create or update credentialissuer: create failed: error on create") - }) - }) - }) - - when("the config already exists", func() { - var existingConfig *configv1alpha1.CredentialIssuer - - it.Before(func() { - existingConfig = &configv1alpha1.CredentialIssuer{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{ - Name: credentialIssuerResourceName, - Labels: map[string]string{ - "myLabelKey1": "myLabelValue1", - }, - }, - Status: configv1alpha1.CredentialIssuerStatus{ - Strategies: []configv1alpha1.CredentialIssuerStrategy{ - { - Type: configv1alpha1.KubeClusterSigningCertificateStrategyType, - Status: configv1alpha1.SuccessStrategyStatus, - Reason: configv1alpha1.FetchedKeyStrategyReason, - Message: "initial-message", - LastUpdateTime: metav1.Now(), - }, - }, - KubeConfigInfo: &configv1alpha1.CredentialIssuerKubeConfigInfo{ - Server: "initial-server-value", - CertificateAuthorityData: "initial-ca-value", - }, - }, - } - r.NoError(pinnipedAPIClient.Tracker().Add(existingConfig)) - }) - - it("updates the existing config to only apply the updates made by the func parameter", func() { - err := CreateOrUpdateCredentialIssuerStatus( - ctx, - credentialIssuerResourceName, - map[string]string{ - "myLabelKey1": "myLabelValue1", - "myLabelKey2": "myLabelValue2", - }, - pinnipedAPIClient, - func(configToUpdate *configv1alpha1.CredentialIssuerStatus) { - configToUpdate.KubeConfigInfo.CertificateAuthorityData = "new-ca-value" - }, - ) - r.NoError(err) - - expectedGetAction := coretesting.NewRootGetAction(credentialIssuerGVR, credentialIssuerResourceName) - - // Only the edited field should be changed. - expectedUpdatedConfig := existingConfig.DeepCopy() - expectedUpdatedConfig.Status.KubeConfigInfo.CertificateAuthorityData = "new-ca-value" - expectedUpdateAction := coretesting.NewRootUpdateSubresourceAction(credentialIssuerGVR, "status", expectedUpdatedConfig) - - r.Equal([]coretesting.Action{expectedGetAction, expectedUpdateAction}, pinnipedAPIClient.Actions()) - }) - - it("avoids the cost of an update if the local updates made by the func parameter did not actually change anything", func() { - err := CreateOrUpdateCredentialIssuerStatus( - ctx, - credentialIssuerResourceName, - map[string]string{}, - pinnipedAPIClient, - func(configToUpdate *configv1alpha1.CredentialIssuerStatus) { - configToUpdate.KubeConfigInfo.CertificateAuthorityData = "initial-ca-value" - - t := configToUpdate.Strategies[0].LastUpdateTime - loc, err := time.LoadLocation("Asia/Shanghai") - r.NoError(err) - configToUpdate.Strategies[0].LastUpdateTime = metav1.NewTime(t.In(loc)) - }, - ) - r.NoError(err) - - expectedGetAction := coretesting.NewRootGetAction(credentialIssuerGVR, credentialIssuerResourceName) - r.Equal([]coretesting.Action{expectedGetAction}, pinnipedAPIClient.Actions()) - }) - - when("there is an unexpected error while getting the existing object", func() { - it.Before(func() { - pinnipedAPIClient.PrependReactor("get", "credentialissuers", func(_ coretesting.Action) (bool, runtime.Object, error) { - return true, nil, fmt.Errorf("error on get") - }) - }) - - it("returns an error", func() { - err := CreateOrUpdateCredentialIssuerStatus( - ctx, - credentialIssuerResourceName, - map[string]string{}, - pinnipedAPIClient, - func(configToUpdate *configv1alpha1.CredentialIssuerStatus) {}, - ) - r.EqualError(err, "could not create or update credentialissuer: get failed: error on get") - }) - }) - - when("there is an unexpected error while updating the existing object", func() { - it.Before(func() { - pinnipedAPIClient.PrependReactor("update", "credentialissuers", func(_ coretesting.Action) (bool, runtime.Object, error) { - return true, nil, fmt.Errorf("error on update") - }) - }) - - it("returns an error", func() { - err := CreateOrUpdateCredentialIssuerStatus( - ctx, - credentialIssuerResourceName, - map[string]string{}, - pinnipedAPIClient, - func(configToUpdate *configv1alpha1.CredentialIssuerStatus) { - configToUpdate.KubeConfigInfo.CertificateAuthorityData = "new-ca-value" - }, - ) - r.EqualError(err, "could not create or update credentialissuer: error on update") - }) - }) - - when("there is a conflict error while updating the existing object on the first try and the next try succeeds", func() { - var slightlyDifferentExistingConfig *configv1alpha1.CredentialIssuer - - it.Before(func() { - hit := false - slightlyDifferentExistingConfig = existingConfig.DeepCopy() - slightlyDifferentExistingConfig.Status.KubeConfigInfo.Server = "some-other-server-value-from-conflicting-update" - - pinnipedAPIClient.PrependReactor("update", "credentialissuers", func(_ coretesting.Action) (bool, runtime.Object, error) { - // Return an error on the first call, then fall through to the default (successful) response. - if !hit { - // Before the update fails, also change the object that will be returned by the next Get(), - // to make sure that the production code does a fresh Get() after detecting a conflict. - r.NoError(pinnipedAPIClient.Tracker().Update(credentialIssuerGVR, slightlyDifferentExistingConfig, "")) - hit = true - return true, nil, apierrors.NewConflict(schema.GroupResource{ - Group: apiregistrationv1.GroupName, - Resource: "credentialissuers", - }, "alphav1.pinniped.dev", fmt.Errorf("there was a conflict")) - } - return false, nil, nil - }) - }) - - it("retries updates on conflict", func() { - err := CreateOrUpdateCredentialIssuerStatus( - ctx, - credentialIssuerResourceName, - map[string]string{ - "myLabelKey1": "myLabelValue1", - "myLabelKey2": "myLabelValue2", - }, - pinnipedAPIClient, - func(configToUpdate *configv1alpha1.CredentialIssuerStatus) { - configToUpdate.KubeConfigInfo.CertificateAuthorityData = "new-ca-value" - }, - ) - r.NoError(err) - - expectedGetAction := coretesting.NewRootGetAction(credentialIssuerGVR, credentialIssuerResourceName) - - // The first attempted update only includes its own edits. - firstExpectedUpdatedConfig := existingConfig.DeepCopy() - firstExpectedUpdatedConfig.Status.KubeConfigInfo.CertificateAuthorityData = "new-ca-value" - firstExpectedUpdateAction := coretesting.NewRootUpdateSubresourceAction(credentialIssuerGVR, "status", firstExpectedUpdatedConfig) - - // Both the edits made by this update and the edits made by the conflicting update should be included. - secondExpectedUpdatedConfig := existingConfig.DeepCopy() - secondExpectedUpdatedConfig.Status.KubeConfigInfo.Server = "some-other-server-value-from-conflicting-update" - secondExpectedUpdatedConfig.Status.KubeConfigInfo.CertificateAuthorityData = "new-ca-value" - secondExpectedUpdateAction := coretesting.NewRootUpdateSubresourceAction(credentialIssuerGVR, "status", secondExpectedUpdatedConfig) - - expectedActions := []coretesting.Action{ - expectedGetAction, - firstExpectedUpdateAction, - expectedGetAction, - secondExpectedUpdateAction, - } - r.Equal(expectedActions, pinnipedAPIClient.Actions()) - }) - }) - }) - }, spec.Parallel(), spec.Report(report.Terminal{})) -} diff --git a/internal/controller/issuerconfig/doc.go b/internal/controller/issuerconfig/doc.go deleted file mode 100644 index a30f1283..00000000 --- a/internal/controller/issuerconfig/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -// Package issuerconfig contains controller(s) for reconciling CredentialIssuer's. -package issuerconfig diff --git a/internal/controller/issuerconfig/update_strategy.go b/internal/controller/issuerconfig/issuerconfig.go similarity index 82% rename from internal/controller/issuerconfig/update_strategy.go rename to internal/controller/issuerconfig/issuerconfig.go index 5d079136..faa14695 100644 --- a/internal/controller/issuerconfig/update_strategy.go +++ b/internal/controller/issuerconfig/issuerconfig.go @@ -1,6 +1,7 @@ // Copyright 2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +// Package issuerconfig contains helpers for updating CredentialIssuer status entries. package issuerconfig import ( @@ -15,23 +16,6 @@ import ( "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" ) -// UpdateStrategy creates or updates the desired strategy in the CredentialIssuer status.strategies field. -// The CredentialIssuer will be created if it does not already exist. -func UpdateStrategy(ctx context.Context, - name string, - credentialIssuerLabels map[string]string, - pinnipedAPIClient versioned.Interface, - strategy v1alpha1.CredentialIssuerStrategy, -) error { - return CreateOrUpdateCredentialIssuerStatus( - ctx, - name, - credentialIssuerLabels, - pinnipedAPIClient, - func(configToUpdate *v1alpha1.CredentialIssuerStatus) { mergeStrategy(configToUpdate, strategy) }, - ) -} - // Update a strategy on an existing CredentialIssuer, merging into any existing strategy entries. func Update(ctx context.Context, client versioned.Interface, issuer *v1alpha1.CredentialIssuer, strategy v1alpha1.CredentialIssuerStrategy) error { // Update the existing object to merge in the new strategy. @@ -58,7 +42,9 @@ func mergeStrategy(configToUpdate *v1alpha1.CredentialIssuerStatus, strategy v1a } } if existing != nil { - strategy.DeepCopyInto(existing) + if !equalExceptLastUpdated(existing, &strategy) { + strategy.DeepCopyInto(existing) + } } else { configToUpdate.Strategies = append(configToUpdate.Strategies, strategy) } @@ -91,3 +77,11 @@ func (s sortableStrategies) Less(i, j int) bool { return s[i].Type < s[j].Type } func (s sortableStrategies) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +func equalExceptLastUpdated(s1, s2 *v1alpha1.CredentialIssuerStrategy) bool { + s1 = s1.DeepCopy() + s2 = s2.DeepCopy() + s1.LastUpdateTime = metav1.Time{} + s2.LastUpdateTime = metav1.Time{} + return apiequality.Semantic.DeepEqual(s1, s2) +} diff --git a/internal/controller/issuerconfig/update_strategy_test.go b/internal/controller/issuerconfig/issuerconfig_test.go similarity index 86% rename from internal/controller/issuerconfig/update_strategy_test.go rename to internal/controller/issuerconfig/issuerconfig_test.go index 16302499..1ef1600d 100644 --- a/internal/controller/issuerconfig/update_strategy_test.go +++ b/internal/controller/issuerconfig/issuerconfig_test.go @@ -125,6 +125,38 @@ func TestMergeStrategy(t *testing.T) { }, }, }, + { + name: "existing entry matches except for LastUpdated time", + configToUpdate: v1alpha1.CredentialIssuerStatus{ + Strategies: []v1alpha1.CredentialIssuerStrategy{ + { + Type: "Type1", + Status: v1alpha1.ErrorStrategyStatus, + Reason: "some starting reason", + Message: "some starting message", + LastUpdateTime: t1, + }, + }, + }, + strategy: v1alpha1.CredentialIssuerStrategy{ + Type: "Type1", + Status: v1alpha1.ErrorStrategyStatus, + Reason: "some starting reason", + Message: "some starting message", + LastUpdateTime: t2, + }, + expected: v1alpha1.CredentialIssuerStatus{ + Strategies: []v1alpha1.CredentialIssuerStrategy{ + { + Type: "Type1", + Status: v1alpha1.ErrorStrategyStatus, + Reason: "some starting reason", + Message: "some starting message", + LastUpdateTime: t1, + }, + }, + }, + }, { name: "new entry among others", configToUpdate: v1alpha1.CredentialIssuerStatus{ diff --git a/internal/controllermanager/prepare_controllers.go b/internal/controllermanager/prepare_controllers.go index f012dd1e..e6b69ec1 100644 --- a/internal/controllermanager/prepare_controllers.go +++ b/internal/controllermanager/prepare_controllers.go @@ -252,16 +252,15 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) { WithController( impersonatorconfig.NewImpersonatorConfigController( c.ServerInstallationInfo.Namespace, - c.NamesConfig.ImpersonationConfigMap, c.NamesConfig.CredentialIssuer, client.Kubernetes, client.PinnipedConcierge, - informers.installationNamespaceK8s.Core().V1().ConfigMaps(), + informers.pinniped.Config().V1alpha1().CredentialIssuers(), informers.installationNamespaceK8s.Core().V1().Services(), informers.installationNamespaceK8s.Core().V1().Secrets(), controllerlib.WithInformer, - controllerlib.WithInitialEvent, c.NamesConfig.ImpersonationLoadBalancerService, + c.NamesConfig.ImpersonationClusterIPService, c.NamesConfig.ImpersonationTLSCertificateSecret, c.NamesConfig.ImpersonationCACertificateSecret, c.Labels, @@ -269,6 +268,7 @@ func PrepareControllers(c *Config) (func(ctx context.Context), error) { impersonator.New, c.NamesConfig.ImpersonationSignerSecret, c.ImpersonationSigningCertProvider, + klogr.New(), ), singletonWorker, ). diff --git a/internal/oidc/token/token_handler_test.go b/internal/oidc/token/token_handler_test.go index 0f64ae43..bca9a3ce 100644 --- a/internal/oidc/token/token_handler_test.go +++ b/internal/oidc/token/token_handler_test.go @@ -799,7 +799,7 @@ func TestTokenExchange(t *testing.T) { require.NoError(t, json.Unmarshal(parsedJWT.UnsafePayloadWithoutVerification(), &tokenClaims)) // Make sure that these are the only fields in the token. - idTokenFields := []string{"sub", "aud", "iss", "jti", "nonce", "auth_time", "exp", "iat", "rat", "groups", "username"} + idTokenFields := []string{"sub", "aud", "iss", "jti", "auth_time", "exp", "iat", "rat", "groups", "username"} require.ElementsMatch(t, idTokenFields, getMapKeys(tokenClaims)) // Assert that the returned token has expected claims values. @@ -808,7 +808,6 @@ func TestTokenExchange(t *testing.T) { require.NotEmpty(t, tokenClaims["exp"]) require.NotEmpty(t, tokenClaims["iat"]) require.NotEmpty(t, tokenClaims["rat"]) - require.Empty(t, tokenClaims["nonce"]) // ID tokens only contain nonce during an authcode exchange require.Len(t, tokenClaims["aud"], 1) require.Contains(t, tokenClaims["aud"], test.requestedAudience) require.Equal(t, goodSubject, tokenClaims["sub"]) @@ -1717,10 +1716,13 @@ func requireValidIDToken( // Note that there is a bug in fosite which prevents the `at_hash` claim from appearing in this ID token // during the initial authcode exchange, but does not prevent `at_hash` from appearing in the refreshed ID token. // We can add a workaround for this later. - idTokenFields := []string{"sub", "aud", "iss", "jti", "nonce", "auth_time", "exp", "iat", "rat", "groups", "username"} + idTokenFields := []string{"sub", "aud", "iss", "jti", "auth_time", "exp", "iat", "rat", "groups", "username"} if wantAtHashClaimInIDToken { idTokenFields = append(idTokenFields, "at_hash") } + if wantNonceValueInIDToken { + idTokenFields = append(idTokenFields, "nonce") + } // make sure that these are the only fields in the token var m map[string]interface{} diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 11f800eb..e58e76b4 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -31,7 +31,6 @@ import ( const ( ldapsScheme = "ldaps" distinguishedNameAttributeName = "dn" - commonNameAttributeName = "cn" searchFilterInterpolationLocationMarker = "{}" groupSearchPageSize = uint32(250) defaultLDAPPort = uint16(389) @@ -367,7 +366,7 @@ func (p *Provider) searchGroupsForUserDN(conn Conn, userDN string) ([]string, er groupAttributeName := p.c.GroupSearch.GroupNameAttribute if len(groupAttributeName) == 0 { - groupAttributeName = commonNameAttributeName + groupAttributeName = distinguishedNameAttributeName } groups := []string{} @@ -396,13 +395,27 @@ func (p *Provider) validateConfig() error { func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(conn Conn, foundUserDN string) error) (string, string, []string, error) { searchResult, err := conn.Search(p.userSearchRequest(username)) if err != nil { - return "", "", nil, fmt.Errorf(`error searching for user "%s": %w`, username, err) + plog.All(`error searching for user`, + "upstreamName", p.GetName(), + "username", username, + "err", err, + ) + return "", "", nil, fmt.Errorf(`error searching for user: %w`, err) } if len(searchResult.Entries) == 0 { - plog.Debug("error finding user: user not found (if this username is valid, please check the user search configuration)", - "upstreamName", p.GetName(), "username", username) + if plog.Enabled(plog.LevelAll) { + plog.All("error finding user: user not found (if this username is valid, please check the user search configuration)", + "upstreamName", p.GetName(), + "username", username, + ) + } else { + plog.Debug("error finding user: user not found (cowardly avoiding printing username because log level is not 'all')", "upstreamName", p.GetName()) + } return "", "", nil, nil } + + // At this point, we have matched at least one entry, so we can be confident that the username is not actually + // someone's password mistakenly entered into the username field, so we can log it without concern. if len(searchResult.Entries) > 1 { return "", "", nil, fmt.Errorf(`searching for user "%s" resulted in %d search results, but expected 1 result`, username, len(searchResult.Entries), @@ -493,7 +506,7 @@ func (p *Provider) userSearchRequestedAttributes() []string { func (p *Provider) groupSearchRequestedAttributes() []string { switch p.c.GroupSearch.GroupNameAttribute { case "": - return []string{commonNameAttributeName} + return []string{} case distinguishedNameAttributeName: return []string{} default: diff --git a/internal/upstreamldap/upstreamldap_test.go b/internal/upstreamldap/upstreamldap_test.go index 35609bcf..c4482fdf 100644 --- a/internal/upstreamldap/upstreamldap_test.go +++ b/internal/upstreamldap/upstreamldap_test.go @@ -315,6 +315,29 @@ func TestEndUserAuthentication(t *testing.T) { r.UID = base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultDNValue)) }), }, + { + name: "when the GroupNameAttribute is empty then it defaults to dn", + username: testUpstreamUsername, + password: testUpstreamPassword, + providerConfig: providerConfig(func(p *ProviderConfig) { + p.GroupSearch.GroupNameAttribute = "" // blank means to use dn + }), + searchMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) + conn.EXPECT().Search(expectedUserSearch(nil)).Return(exampleUserSearchResult, nil).Times(1) + conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { + r.Attributes = []string{} + }), expectedGroupSearchPageSize). + Return(exampleGroupSearchResult, nil).Times(1) + conn.EXPECT().Close().Times(1) + }, + bindEndUserMocks: func(conn *mockldapconn.MockConn) { + conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) + }, + wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { + r.Groups = []string{testGroupSearchResultDNValue1, testGroupSearchResultDNValue2} + }), + }, { name: "when the GroupNameAttribute is dn", username: testUpstreamUsername, @@ -339,11 +362,11 @@ func TestEndUserAuthentication(t *testing.T) { }), }, { - name: "when the GroupNameAttribute is empty then it defaults to cn", + name: "when the GroupNameAttribute is cn", username: testUpstreamUsername, password: testUpstreamPassword, providerConfig: providerConfig(func(p *ProviderConfig) { - p.GroupSearch.GroupNameAttribute = "" // blank means to use cn + p.GroupSearch.GroupNameAttribute = "cn" }), searchMocks: func(conn *mockldapconn.MockConn) { conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) @@ -523,7 +546,7 @@ func TestEndUserAuthentication(t *testing.T) { conn.EXPECT().Search(expectedUserSearch(nil)).Return(nil, errors.New("some user search error")).Times(1) conn.EXPECT().Close().Times(1) }, - wantError: fmt.Sprintf(`error searching for user "%s": some user search error`, testUpstreamUsername), + wantError: `error searching for user: some user search error`, }, { name: "when searching for the user's groups returns an error", diff --git a/site/content/docs/howto/install-concierge.md b/site/content/docs/howto/install-concierge.md index 7eed25d7..9fd06065 100644 --- a/site/content/docs/howto/install-concierge.md +++ b/site/content/docs/howto/install-concierge.md @@ -17,6 +17,8 @@ You should have a [supported Kubernetes cluster]({{< ref "../reference/supported 1. Install the latest version of the Concierge into the `pinniped-concierge` namespace with default options: - `kubectl apply -f https://get.pinniped.dev/latest/install-pinniped-concierge.yaml` + +Warning: the default configuration may create a public LoadBalancer Service on your cluster. ## With specific version and default options diff --git a/test/cluster_capabilities/aks.yaml b/test/cluster_capabilities/aks.yaml index cc3f68ed..2a944bda 100644 --- a/test/cluster_capabilities/aks.yaml +++ b/test/cluster_capabilities/aks.yaml @@ -13,3 +13,6 @@ capabilities: # Does the cluster allow requests without authentication? # https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests anonymousAuthenticationSupported: false + + # Are LDAP ports on the Internet reachable without interference from network firewalls or proxies? + canReachInternetLDAPPorts: true diff --git a/test/cluster_capabilities/eks.yaml b/test/cluster_capabilities/eks.yaml index 6d545f2f..9bce553d 100644 --- a/test/cluster_capabilities/eks.yaml +++ b/test/cluster_capabilities/eks.yaml @@ -13,3 +13,6 @@ capabilities: # Does the cluster allow requests without authentication? # https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests anonymousAuthenticationSupported: true + + # Are LDAP ports on the Internet reachable without interference from network firewalls or proxies? + canReachInternetLDAPPorts: true diff --git a/test/cluster_capabilities/gke.yaml b/test/cluster_capabilities/gke.yaml index 8bba8b8d..080cec4a 100644 --- a/test/cluster_capabilities/gke.yaml +++ b/test/cluster_capabilities/gke.yaml @@ -13,3 +13,6 @@ capabilities: # Does the cluster allow requests without authentication? # https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests anonymousAuthenticationSupported: true + + # Are LDAP ports on the Internet reachable without interference from network firewalls or proxies? + canReachInternetLDAPPorts: true diff --git a/test/cluster_capabilities/kind.yaml b/test/cluster_capabilities/kind.yaml index 0724edb9..92759ed9 100644 --- a/test/cluster_capabilities/kind.yaml +++ b/test/cluster_capabilities/kind.yaml @@ -13,3 +13,6 @@ capabilities: # Does the cluster allow requests without authentication? # https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests anonymousAuthenticationSupported: true + + # Are LDAP ports on the Internet reachable without interference from network firewalls or proxies? + canReachInternetLDAPPorts: true diff --git a/test/cluster_capabilities/tkgs.yaml b/test/cluster_capabilities/tkgs.yaml index 2ea82b1e..4c7d05ba 100644 --- a/test/cluster_capabilities/tkgs.yaml +++ b/test/cluster_capabilities/tkgs.yaml @@ -13,3 +13,6 @@ capabilities: # Does the cluster allow requests without authentication? # https://kubernetes.io/docs/reference/access-authn-authz/authentication/#anonymous-requests anonymousAuthenticationSupported: true + + # Are LDAP ports on the Internet reachable without interference from network firewalls or proxies? + canReachInternetLDAPPorts: false diff --git a/test/integration/concierge_impersonation_proxy_test.go b/test/integration/concierge_impersonation_proxy_test.go index faf44e95..7130e17e 100644 --- a/test/integration/concierge_impersonation_proxy_test.go +++ b/test/integration/concierge_impersonation_proxy_test.go @@ -21,6 +21,7 @@ import ( "os" "os/exec" "path/filepath" + "reflect" "strings" "sync" "testing" @@ -51,13 +52,12 @@ import ( "k8s.io/client-go/util/cert" "k8s.io/client-go/util/certificate/csr" "k8s.io/client-go/util/keyutil" - "sigs.k8s.io/yaml" + "k8s.io/client-go/util/retry" conciergev1alpha "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1" identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1" loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1" pinnipedconciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned" - "go.pinniped.dev/internal/concierge/impersonator" "go.pinniped.dev/internal/httputil/roundtripper" "go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/testutil" @@ -121,7 +121,10 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl mostRecentTokenCredentialRequestResponse *loginv1alpha1.TokenCredentialRequest mostRecentTokenCredentialRequestResponseLock sync.Mutex ) - refreshCredential := func(t *testing.T, impersonationProxyURL string, impersonationProxyCACertPEM []byte) *loginv1alpha1.ClusterCredential { + + refreshCredentialHelper := func(t *testing.T, client pinnipedconciergeclientset.Interface) *loginv1alpha1.ClusterCredential { + t.Helper() + mostRecentTokenCredentialRequestResponseLock.Lock() defer mostRecentTokenCredentialRequestResponseLock.Unlock() if mostRecentTokenCredentialRequestResponse == nil || credentialAlmostExpired(t, mostRecentTokenCredentialRequestResponse) { @@ -133,18 +136,13 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl // However, we issue short-lived certs, so this cert will only be valid for a few minutes. // Cache it until it is almost expired and then refresh it whenever it is close to expired. // - // Also, use an anonymous client which goes through the impersonation proxy to make the request because that's - // what would normally happen when a user is using a kubeconfig where the server is the impersonation proxy, - // so it more closely simulates the normal use case, and also because we want this to work on AKS clusters - // which do not allow anonymous requests. - client := newAnonymousImpersonationProxyClient(t, impersonationProxyURL, impersonationProxyCACertPEM, nil).PinnipedConcierge require.Eventually(t, func() bool { mostRecentTokenCredentialRequestResponse, err = createTokenCredentialRequest(credentialRequestSpecWithWorkingCredentials, client) if err != nil { t.Logf("failed to make TokenCredentialRequest: %s", library.Sdump(err)) return false } - return true + return mostRecentTokenCredentialRequestResponse.Status.Credential != nil }, 5*time.Minute, 5*time.Second) require.Nil(t, mostRecentTokenCredentialRequestResponse.Status.Message, @@ -156,36 +154,38 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl // tokens, we should revisit this test's rest config below. require.Empty(t, mostRecentTokenCredentialRequestResponse.Status.Credential.Token) } + return mostRecentTokenCredentialRequestResponse.Status.Credential } - oldConfigMap, err := adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Get(ctx, impersonationProxyConfigMapName(env), metav1.GetOptions{}) - if !k8serrors.IsNotFound(err) { - require.NoError(t, err) // other errors aside from NotFound are unexpected - t.Logf("stashing a pre-existing configmap %s", oldConfigMap.Name) - require.NoError(t, adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Delete(ctx, impersonationProxyConfigMapName(env), metav1.DeleteOptions{})) + refreshCredential := func(t *testing.T, impersonationProxyURL string, impersonationProxyCACertPEM []byte) *loginv1alpha1.ClusterCredential { + // Use an anonymous client which goes through the impersonation proxy to make the request because that's + // what would normally happen when a user is using a kubeconfig where the server is the impersonation proxy, + // so it more closely simulates the normal use case, and also because we want this to work on AKS clusters + // which do not allow anonymous requests. + client := newAnonymousImpersonationProxyClient(t, impersonationProxyURL, impersonationProxyCACertPEM, nil).PinnipedConcierge + return refreshCredentialHelper(t, client) } - // At the end of the test, clean up the ConfigMap. + + oldCredentialIssuer, err := adminConciergeClient.ConfigV1alpha1().CredentialIssuers().Get(ctx, credentialIssuerName(env), metav1.GetOptions{}) + require.NoError(t, err) + // At the end of the test, clean up the CredentialIssuer t.Cleanup(func() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() // Delete any version that was created by this test. - t.Logf("cleaning up configmap at end of test %s", impersonationProxyConfigMapName(env)) - err := adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Delete(ctx, impersonationProxyConfigMapName(env), metav1.DeleteOptions{}) - if !k8serrors.IsNotFound(err) { - require.NoError(t, err) // only not found errors are acceptable - } - - // Only recreate it if it already existed at the start of this test. - if len(oldConfigMap.Data) != 0 { - t.Log(oldConfigMap) - oldConfigMap.UID = "" // cant have a UID yet - oldConfigMap.ResourceVersion = "" - t.Logf("restoring a pre-existing configmap %s", oldConfigMap.Name) - _, err = adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Create(ctx, oldConfigMap, metav1.CreateOptions{}) - require.NoError(t, err) - } + t.Logf("cleaning up credentialissuer at end of test %s", credentialIssuerName(env)) + err = retry.RetryOnConflict(retry.DefaultRetry, func() error { + newCredentialIssuer, err := adminConciergeClient.ConfigV1alpha1().CredentialIssuers().Get(ctx, credentialIssuerName(env), metav1.GetOptions{}) + if err != nil { + return err + } + oldCredentialIssuer.Spec.DeepCopyInto(&newCredentialIssuer.Spec) + _, err = adminConciergeClient.ConfigV1alpha1().CredentialIssuers().Update(ctx, newCredentialIssuer, metav1.UpdateOptions{}) + return err + }) + require.NoError(t, err) // If we are running on an environment that has a load balancer, expect that the // CredentialIssuer will be updated eventually with a successful impersonation proxy frontend. @@ -200,6 +200,15 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl // this point depending on the capabilities of the cluster under test. We handle each possible case here. switch { case impersonatorShouldHaveStartedAutomaticallyByDefault && clusterSupportsLoadBalancers: + // configure the credential issuer spec to have the impersonation proxy in auto mode + updateCredentialIssuer(ctx, t, env, adminConciergeClient, conciergev1alpha.CredentialIssuerSpec{ + ImpersonationProxy: &conciergev1alpha.ImpersonationProxySpec{ + Mode: conciergev1alpha.ImpersonationProxyModeAuto, + Service: conciergev1alpha.ImpersonationProxyServiceSpec{ + Type: conciergev1alpha.ImpersonationProxyServiceTypeLoadBalancer, + }, + }, + }) // Auto mode should have decided that the impersonator will run and should have started a load balancer, // and we will be able to use the load balancer to access the impersonator. (e.g. GKE, AKS, EKS) // Check that load balancer has been automatically created by the impersonator's "auto" mode. @@ -221,10 +230,11 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl requireDisabledStrategy(ctx, t, env, adminConciergeClient) // Create configuration to make the impersonation proxy turn on with no endpoint (i.e. automatically create a load balancer). - configMap := impersonationProxyConfigMapForConfig(t, env, impersonator.Config{Mode: impersonator.ModeEnabled}) - t.Logf("creating configmap %s", configMap.Name) - _, err = adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Create(ctx, &configMap, metav1.CreateOptions{}) - require.NoError(t, err) + updateCredentialIssuer(ctx, t, env, adminConciergeClient, conciergev1alpha.CredentialIssuerSpec{ + ImpersonationProxy: &conciergev1alpha.ImpersonationProxySpec{ + Mode: conciergev1alpha.ImpersonationProxyModeEnabled, + }, + }) default: // Auto mode should have decided that the impersonator will be disabled. We need to manually enable it. @@ -246,13 +256,12 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl require.Truef(t, isErr, "wanted error %q to be service unavailable via squid error, but: %s", err, message) // Create configuration to make the impersonation proxy turn on with a hard coded endpoint (without a load balancer). - configMap := impersonationProxyConfigMapForConfig(t, env, impersonator.Config{ - Mode: impersonator.ModeEnabled, - Endpoint: proxyServiceEndpoint, + updateCredentialIssuer(ctx, t, env, adminConciergeClient, conciergev1alpha.CredentialIssuerSpec{ + ImpersonationProxy: &conciergev1alpha.ImpersonationProxySpec{ + Mode: conciergev1alpha.ImpersonationProxyModeEnabled, + ExternalEndpoint: proxyServiceEndpoint, + }, }) - t.Logf("creating configmap %s", configMap.Name) - _, err = adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Create(ctx, &configMap, metav1.CreateOptions{}) - require.NoError(t, err) } // At this point the impersonator should be starting/running. When it is ready, the CredentialIssuer's @@ -376,6 +385,24 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl t.Parallel() kubeconfigPath, envVarsWithProxy, _ := getImpersonationKubeconfig(t, env, impersonationProxyURL, impersonationProxyCACertPEM, credentialRequestSpecWithWorkingCredentials.Authenticator) + // set credential issuer to have new annotation to ensure the idle timeout is long enough on AWS. + // this also ensures that updates to the credential issuer impersonation proxy spec go through + if impersonatorShouldHaveStartedAutomaticallyByDefault && clusterSupportsLoadBalancers { + updateCredentialIssuer(ctx, t, env, adminConciergeClient, conciergev1alpha.CredentialIssuerSpec{ + ImpersonationProxy: &conciergev1alpha.ImpersonationProxySpec{ + Mode: conciergev1alpha.ImpersonationProxyModeAuto, + Service: conciergev1alpha.ImpersonationProxyServiceSpec{ + Type: conciergev1alpha.ImpersonationProxyServiceTypeLoadBalancer, + Annotations: map[string]string{"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "4000"}, + }, + }, + }) + // Wait until the annotation shows up on the load balancer + library.RequireEventuallyWithoutError(t, func() (bool, error) { + return loadBalancerHasAnnotations(ctx, env, adminClient, map[string]string{"service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout": "4000"}) + }, 30*time.Second, 500*time.Millisecond) + } + // Run the kubectl port-forward command. timeout, cancelFunc := context.WithTimeout(ctx, 2*time.Minute) defer cancelFunc() @@ -1181,18 +1208,47 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl }) }) + t.Run("running impersonation proxy with ClusterIP service", func(t *testing.T) { + if env.Proxy == "" { + t.Skip("Skipping ClusterIP test because squid proxy is not present") + } + clusterIPServiceURL := fmt.Sprintf("%s.%s.svc.cluster.local", impersonationProxyClusterIPName(env), env.ConciergeNamespace) + updateCredentialIssuer(ctx, t, env, adminConciergeClient, conciergev1alpha.CredentialIssuerSpec{ + ImpersonationProxy: &conciergev1alpha.ImpersonationProxySpec{ + Mode: conciergev1alpha.ImpersonationProxyModeEnabled, + ExternalEndpoint: clusterIPServiceURL, + Service: conciergev1alpha.ImpersonationProxyServiceSpec{ + Type: conciergev1alpha.ImpersonationProxyServiceTypeClusterIP, + }, + }, + }) + + // wait until the credential issuer is updated with the new url + require.Eventually(t, func() bool { + newImpersonationProxyURL, _ := performImpersonatorDiscovery(ctx, t, env, adminConciergeClient) + return newImpersonationProxyURL == "https://"+clusterIPServiceURL + }, 30*time.Second, 500*time.Millisecond) + newImpersonationProxyURL, newImpersonationProxyCACertPEM := performImpersonatorDiscovery(ctx, t, env, adminConciergeClient) + + anonymousClient := newAnonymousImpersonationProxyClientWithProxy(t, newImpersonationProxyURL, newImpersonationProxyCACertPEM, nil).PinnipedConcierge + refreshedCredentials := refreshCredentialHelper(t, anonymousClient) + + client := newImpersonationProxyClientWithCredentialsAndProxy(t, refreshedCredentials, newImpersonationProxyURL, newImpersonationProxyCACertPEM, nil).Kubernetes + + // everything should work properly through the cluster ip service + t.Run( + "access as user", + library.AccessAsUserTest(ctx, env.TestUser.ExpectedUsername, client), + ) + }) + t.Run("manually disabling the impersonation proxy feature", func(t *testing.T) { // Update configuration to force the proxy to disabled mode - configMap := impersonationProxyConfigMapForConfig(t, env, impersonator.Config{Mode: impersonator.ModeDisabled}) - if clusterSupportsLoadBalancers { - t.Logf("creating configmap %s", configMap.Name) - _, err := adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Create(ctx, &configMap, metav1.CreateOptions{}) - require.NoError(t, err) - } else { - t.Logf("updating configmap %s", configMap.Name) - _, err := adminClient.CoreV1().ConfigMaps(env.ConciergeNamespace).Update(ctx, &configMap, metav1.UpdateOptions{}) - require.NoError(t, err) - } + updateCredentialIssuer(ctx, t, env, adminConciergeClient, conciergev1alpha.CredentialIssuerSpec{ + ImpersonationProxy: &conciergev1alpha.ImpersonationProxySpec{ + Mode: conciergev1alpha.ImpersonationProxyModeDisabled, + }, + }) if clusterSupportsLoadBalancers { // The load balancer should have been deleted when we disabled the impersonation proxy. @@ -1450,19 +1506,19 @@ func kubeconfigProxyFunc(t *testing.T, squidProxyURL string) func(req *http.Requ } } -func impersonationProxyConfigMapForConfig(t *testing.T, env *library.TestEnv, config impersonator.Config) corev1.ConfigMap { +func updateCredentialIssuer(ctx context.Context, t *testing.T, env *library.TestEnv, adminConciergeClient pinnipedconciergeclientset.Interface, spec conciergev1alpha.CredentialIssuerSpec) { t.Helper() - configString, err := yaml.Marshal(config) + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + newCredentialIssuer, err := adminConciergeClient.ConfigV1alpha1().CredentialIssuers().Get(ctx, credentialIssuerName(env), metav1.GetOptions{}) + if err != nil { + return err + } + spec.DeepCopyInto(&newCredentialIssuer.Spec) + _, err = adminConciergeClient.ConfigV1alpha1().CredentialIssuers().Update(ctx, newCredentialIssuer, metav1.UpdateOptions{}) + return err + }) require.NoError(t, err) - configMap := corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: impersonationProxyConfigMapName(env), - }, - Data: map[string]string{ - "config.yaml": string(configString), - }} - return configMap } func hasImpersonationProxyLoadBalancerService(ctx context.Context, env *library.TestEnv, client kubernetes.Interface) (bool, error) { @@ -1476,8 +1532,16 @@ func hasImpersonationProxyLoadBalancerService(ctx context.Context, env *library. return service.Spec.Type == corev1.ServiceTypeLoadBalancer, nil } -func impersonationProxyConfigMapName(env *library.TestEnv) string { - return env.ConciergeAppName + "-impersonation-proxy-config" +func loadBalancerHasAnnotations(ctx context.Context, env *library.TestEnv, client kubernetes.Interface, annotations map[string]string) (bool, error) { + service, err := client.CoreV1().Services(env.ConciergeNamespace).Get(ctx, impersonationProxyLoadBalancerName(env), metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + hasExactAnnotations := reflect.DeepEqual(annotations, service.Annotations) + return service.Spec.Type == corev1.ServiceTypeLoadBalancer && hasExactAnnotations, nil } func impersonationProxyTLSSecretName(env *library.TestEnv) string { @@ -1492,6 +1556,10 @@ func impersonationProxyLoadBalancerName(env *library.TestEnv) string { return env.ConciergeAppName + "-impersonation-proxy-load-balancer" } +func impersonationProxyClusterIPName(env *library.TestEnv) string { + return env.ConciergeAppName + "-impersonation-proxy-cluster-ip" +} + func credentialIssuerName(env *library.TestEnv) string { return env.ConciergeAppName + "-config" } @@ -1643,6 +1711,29 @@ func newAnonymousImpersonationProxyClient(t *testing.T, impersonationProxyURL st return newImpersonationProxyClientWithCredentials(t, emptyCredentials, impersonationProxyURL, impersonationProxyCACertPEM, nestedImpersonationConfig) } +func newImpersonationProxyClientWithCredentialsAndProxy(t *testing.T, credentials *loginv1alpha1.ClusterCredential, impersonationProxyURL string, impersonationProxyCACertPEM []byte, nestedImpersonationConfig *rest.ImpersonationConfig) *kubeclient.Client { + t.Helper() + + env := library.IntegrationEnv(t) + + kubeconfig := impersonationProxyRestConfig(credentials, impersonationProxyURL, impersonationProxyCACertPEM, nestedImpersonationConfig) + kubeconfig.Proxy = kubeconfigProxyFunc(t, env.Proxy) + return library.NewKubeclient(t, kubeconfig) +} + +// this uses a proxy in all cases, the other will only use it if you don't have load balancer capabilities. +func newAnonymousImpersonationProxyClientWithProxy(t *testing.T, impersonationProxyURL string, impersonationProxyCACertPEM []byte, nestedImpersonationConfig *rest.ImpersonationConfig) *kubeclient.Client { + t.Helper() + env := library.IntegrationEnv(t) + + emptyCredentials := &loginv1alpha1.ClusterCredential{} + kubeconfig := impersonationProxyRestConfig(emptyCredentials, impersonationProxyURL, impersonationProxyCACertPEM, nestedImpersonationConfig) + + kubeconfig.Proxy = kubeconfigProxyFunc(t, env.Proxy) + + return library.NewKubeclient(t, kubeconfig) +} + func impersonationProxyViaSquidKubeClientWithoutCredential(t *testing.T, proxyServiceEndpoint string) kubernetes.Interface { t.Helper() diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index 98c72ec0..fd9b450a 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -277,8 +277,12 @@ func TestE2EFullIntegration(t *testing.T) { // Add an LDAP upstream IDP and try using it to authenticate during kubectl commands. t.Run("with Supervisor LDAP upstream IDP", func(t *testing.T) { + if len(env.ToolsNamespace) == 0 && !env.HasCapability(library.CanReachInternetLDAPPorts) { + t.Skip("LDAP integration test requires connectivity to an LDAP server") + } + expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue - expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs + expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. library.CreateTestClusterRoleBinding(t, @@ -321,7 +325,7 @@ func TestE2EFullIntegration(t *testing.T) { Base: env.SupervisorUpstreamLDAP.GroupSearchBase, Filter: "", // use the default value of "member={}" Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{ - GroupName: "", // use the default value of "cn" + GroupName: "", // use the default value of "dn" }, }, }, idpv1alpha1.LDAPPhaseReady) diff --git a/test/integration/kubeclient_test.go b/test/integration/kubeclient_test.go index 88edc854..9d57bd08 100644 --- a/test/integration/kubeclient_test.go +++ b/test/integration/kubeclient_test.go @@ -228,6 +228,11 @@ func TestKubeClientOwnerRef(t *testing.T) { GenerateName: "owner-ref-test-", OwnerReferences: nil, // no owner refs set }, + Spec: conciergeconfigv1alpha1.CredentialIssuerSpec{ + ImpersonationProxy: &conciergeconfigv1alpha1.ImpersonationProxySpec{ + Mode: conciergeconfigv1alpha1.ImpersonationProxyModeDisabled, + }, + }, }, metav1.CreateOptions{}, ) diff --git a/test/integration/ldap_client_test.go b/test/integration/ldap_client_test.go index 5e4735c3..e972fc49 100644 --- a/test/integration/ldap_client_test.go +++ b/test/integration/ldap_client_test.go @@ -243,6 +243,20 @@ func TestLDAPSearch(t *testing.T) { }}, }, }, + { + name: "using the default group name attribute, which is dn", + username: "pinny", + password: pinnyPassword, + provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { + p.GroupSearch.GroupNameAttribute = "" + })), + wantAuthResponse: &authenticator.Response{ + User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{ + "cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev", + "cn=seals,ou=groups,dc=pinniped,dc=dev", + }}, + }, + }, { name: "using some other custom group name attribute", username: "pinny", @@ -334,7 +348,7 @@ func TestLDAPSearch(t *testing.T) { username: "pinny", password: pinnyPassword, provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "*" })), - wantError: `error searching for user "pinny": LDAP Result Code 201 "Filter Compile Error": ldap: error parsing filter`, + wantError: `error searching for user: LDAP Result Code 201 "Filter Compile Error": ldap: error parsing filter`, }, { name: "when the group search filter does not compile", @@ -350,7 +364,7 @@ func TestLDAPSearch(t *testing.T) { provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "objectClass=*" // overly broad search filter })), - wantError: `error searching for user "pinny": LDAP Result Code 4 "Size Limit Exceeded": `, + wantError: `error searching for user: LDAP Result Code 4 "Size Limit Exceeded": `, }, { name: "when the server is unreachable with TLS", @@ -522,7 +536,7 @@ func TestLDAPSearch(t *testing.T) { username: "pinny", password: pinnyPassword, provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "invalid-base" })), - wantError: `error searching for user "pinny": LDAP Result Code 34 "Invalid DN Syntax": invalid DN`, + wantError: `error searching for user: LDAP Result Code 34 "Invalid DN Syntax": invalid DN`, }, { name: "when the group search base is invalid", @@ -536,7 +550,7 @@ func TestLDAPSearch(t *testing.T) { username: "pinny", password: pinnyPassword, provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "ou=does-not-exist,dc=pinniped,dc=dev" })), - wantError: `error searching for user "pinny": LDAP Result Code 32 "No Such Object": `, + wantError: `error searching for user: LDAP Result Code 32 "No Such Object": `, }, { name: "when the group search base does not exist", @@ -675,8 +689,8 @@ func defaultProviderConfig(env *library.TestEnv, port string) *upstreamldap.Prov }, GroupSearch: upstreamldap.GroupSearchConfig{ Base: "ou=groups,dc=pinniped,dc=dev", - Filter: "", // defaults to member={} - GroupNameAttribute: "", // defaults to cn + Filter: "", // defaults to member={} + GroupNameAttribute: "cn", // defaults to dn, but here we set it to cn }, } } diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index 5e550580..692b0db8 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -41,6 +41,7 @@ func TestSupervisorLogin(t *testing.T) { tests := []struct { name string + maybeSkip func(t *testing.T) createIDP func(t *testing.T) requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client) wantDownstreamIDTokenSubjectToMatch string @@ -48,7 +49,7 @@ func TestSupervisorLogin(t *testing.T) { wantDownstreamIDTokenGroups []string }{ { - name: "oidc", + name: "oidc with default username and groups claim settings", createIDP: func(t *testing.T) { t.Helper() library.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ @@ -66,10 +67,41 @@ func TestSupervisorLogin(t *testing.T) { wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", // the ID token Username should include the upstream user ID after the upstream issuer name wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + }, + { + name: "oidc with custom username and groups claim settings", + createIDP: func(t *testing.T) { + t.Helper() + library.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{ + Issuer: env.SupervisorUpstreamOIDC.Issuer, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)), + }, + Client: idpv1alpha1.OIDCClient{ + SecretName: library.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name, + }, + Claims: idpv1alpha1.OIDCClaims{ + Username: env.SupervisorUpstreamOIDC.UsernameClaim, + Groups: env.SupervisorUpstreamOIDC.GroupsClaim, + }, + AuthorizationConfig: idpv1alpha1.OIDCAuthorizationConfig{ + AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes, + }, + }, idpv1alpha1.PhaseReady) + }, + requestAuthorization: requestAuthorizationUsingOIDCIdentityProvider, + wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+", + wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Username), wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups, }, { name: "ldap with email as username and groups names as DNs and using an LDAP provider which supports TLS", + maybeSkip: func(t *testing.T) { + t.Helper() + if len(env.ToolsNamespace) == 0 && !env.HasCapability(library.CanReachInternetLDAPPorts) { + t.Skip("LDAP integration test requires connectivity to an LDAP server") + } + }, createIDP: func(t *testing.T) { t.Helper() secret := library.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, @@ -129,6 +161,12 @@ func TestSupervisorLogin(t *testing.T) { }, { name: "ldap with CN as username and group names as CNs and using an LDAP provider which only supports StartTLS", // try another variation of configuration options + maybeSkip: func(t *testing.T) { + t.Helper() + if len(env.ToolsNamespace) == 0 && !env.HasCapability(library.CanReachInternetLDAPPorts) { + t.Skip("LDAP integration test requires connectivity to an LDAP server") + } + }, createIDP: func(t *testing.T) { t.Helper() secret := library.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth, @@ -188,14 +226,16 @@ func TestSupervisorLogin(t *testing.T) { }, } for _, test := range tests { - test := test - t.Run(test.name, func(t *testing.T) { + tt := test + t.Run(tt.name, func(t *testing.T) { + tt.maybeSkip(t) + testSupervisorLogin(t, - test.createIDP, - test.requestAuthorization, - test.wantDownstreamIDTokenSubjectToMatch, - test.wantDownstreamIDTokenUsernameToMatch, - test.wantDownstreamIDTokenGroups, + tt.createIDP, + tt.requestAuthorization, + tt.wantDownstreamIDTokenSubjectToMatch, + tt.wantDownstreamIDTokenUsernameToMatch, + tt.wantDownstreamIDTokenGroups, ) }) } @@ -383,10 +423,11 @@ func testSupervisorLogin( refreshedTokenResponse, err := refreshSource.Token() require.NoError(t, err) - expectedIDTokenClaims = append(expectedIDTokenClaims, "at_hash") + // When refreshing, expect to get an "at_hash" claim, but no "nonce" claim. + expectRefreshedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "rat", "username", "groups", "at_hash"} verifyTokenResponse(t, refreshedTokenResponse, discovery, downstreamOAuth2Config, "", - expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups) + expectRefreshedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups) require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken) require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken) diff --git a/test/library/env.go b/test/library/env.go index d8e7b44c..00a8c1e4 100644 --- a/test/library/env.go +++ b/test/library/env.go @@ -24,6 +24,7 @@ const ( ClusterSigningKeyIsAvailable Capability = "clusterSigningKeyIsAvailable" AnonymousAuthenticationSupported Capability = "anonymousAuthenticationSupported" HasExternalLoadBalancerProvider Capability = "hasExternalLoadBalancerProvider" + CanReachInternetLDAPPorts Capability = "canReachInternetLDAPPorts" ) // TestEnv captures all the external parameters consumed by our integration tests.