diff --git a/apis/concierge/config/v1alpha1/types_credentialissuer.go.tmpl b/apis/concierge/config/v1alpha1/types_credentialissuer.go.tmpl index 3c0c8ba03..60edeccc4 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/deploy/concierge/config.concierge.pinniped.dev_credentialissuers.yaml b/deploy/concierge/config.concierge.pinniped.dev_credentialissuers.yaml index 8123f2385..7b94b098c 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 8add6eddf..31c7fe683 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 05d550dbd..0445afb25 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/generated/1.17/README.adoc b/generated/1.17/README.adoc index b08040ec8..58890ee64 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 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 3c0c8ba03..60edeccc4 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 4a3ad605e..cf679b2c8 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/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.17/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8123f2385..7b94b098c 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.18/README.adoc b/generated/1.18/README.adoc index d901b8be0..70b74f903 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 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 3c0c8ba03..60edeccc4 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 4a3ad605e..cf679b2c8 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/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.18/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8123f2385..7b94b098c 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.19/README.adoc b/generated/1.19/README.adoc index 0e1f160b2..104274cad 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 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 3c0c8ba03..60edeccc4 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 4a3ad605e..cf679b2c8 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/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.19/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8123f2385..7b94b098c 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.20/README.adoc b/generated/1.20/README.adoc index 103b809c6..005bcc7cb 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 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 3c0c8ba03..60edeccc4 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 4a3ad605e..cf679b2c8 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/crds/config.concierge.pinniped.dev_credentialissuers.yaml b/generated/1.20/crds/config.concierge.pinniped.dev_credentialissuers.yaml index 8123f2385..7b94b098c 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/latest/apis/concierge/config/v1alpha1/types_credentialissuer.go b/generated/latest/apis/concierge/config/v1alpha1/types_credentialissuer.go index 3c0c8ba03..60edeccc4 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 4a3ad605e..cf679b2c8 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/internal/concierge/impersonator/config.go b/internal/concierge/impersonator/config.go deleted file mode 100644 index 60ab59bd6..000000000 --- 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 b734dba91..000000000 --- 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 5778b795d..2eeaf25d6 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 ad6a00df3..cbe9d7f9e 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 baebf8c37..c29fb5f6f 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 ecd36d0a1..6aa6733ad 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 c94aae15d..29e7dfa73 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 c7f7767a9..91db0a761 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 7d936b3ab..000000000 --- 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 beb20e56e..000000000 --- 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 a30f1283e..000000000 --- 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 5d0791363..faa146955 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 163024993..1ef1600d9 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 f012dd1ed..e6b69ec10 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/site/content/docs/howto/install-concierge.md b/site/content/docs/howto/install-concierge.md index 7eed25d70..9fd060651 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/integration/concierge_impersonation_proxy_test.go b/test/integration/concierge_impersonation_proxy_test.go index faf44e95f..7130e17ee 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/kubeclient_test.go b/test/integration/kubeclient_test.go index 88edc8541..9d57bd08a 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{}, )