From 8c1abbb4ca42bbefb97b1c24642dcad228e9b5a2 Mon Sep 17 00:00:00 2001 From: Howard Burgess Date: Tue, 23 Nov 2021 15:53:36 +0000 Subject: [PATCH] Fetch API server URL from GKE OIDC ClientConfig (#73) This allows osprey client to fetch the API Server URL from the kube-public/ClientConfig resources that is added by enabling the OIDC Identity Service in GKE. The CA is also fetched from the ClientConfig resource. --- CHANGELOG.md | 4 ++ README.md | 15 +++-- client/azure.go | 55 +++++++++++++++++- client/config.go | 4 ++ client/osprey.go | 6 +- client/target.go | 7 +++ e2e/apiservertest/server.go | 112 ++++++++++++++++++++++++++++++++++-- e2e/e2e_suite_test.go | 24 ++++---- e2e/login_test.go | 8 +-- e2e/logout_test.go | 2 +- e2e/oidc_login_test.go | 59 +++++++++++++++---- e2e/ospreytest/server.go | 12 ++-- e2e/targets_test.go | 2 +- e2e/user_test.go | 2 +- 14 files changed, 266 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6902a1a..6534ffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# Release 2.6.0 +- Allow osprey client to retrieve the API server URL and CA cert from the GKE-specific + OIDC ClientConfig resource. See the `use-gke-clientconfig` osprey config element. + # Release 2.5.0 - Add ability for osprey client to fetch the API server CA from the API server itself, rather than needing an osprey server deployment to serve it. See the Kubernetes feature diff --git a/README.md b/README.md index c42a239..9212bee 100644 --- a/README.md +++ b/README.md @@ -282,10 +282,17 @@ providers: targets: foo.cluster: server: http://osprey.foo.cluster - # If api-server is specified, osprey will fetch the CA cert from the API server itself. Overrides "server". - # A ConfigMap in kube-publiuc called kube-root-ca.crt should be made accessible to system:anonymous - # This ConfigMap is created automatically with the Kubernetes feature gate RootCAConfigMap which was - # alpha in Kubernetes v1.13 and became enabled by default in v1.20+ + # If use-gke-clientconfig is specified (default false) osprey will fetch the API server URL and its + # CA cert from the GKE-specific ClientConfig resource in kube-public. This resource is created automatically + # by GKE when you enable to OIDC Identity Service. The api-server config element is also required. + # Usually api-server would be set to the public API server endpoint; the fetched API server URL will be + # the internal load balancer that proxies requests through the OIDC service. + # use-gke-clientconfig: true + # + # If api-server is specified (default ""), osprey will fetch the CA cert from the API server itself. + # Overrides "server". A ConfigMap in kube-publiuc called kube-root-ca.crt should be made accessible + # to the system:anonymous group. This ConfigMap is created automatically with the Kubernetes feature + # gate RootCAConfigMap which was alpha in Kubernetes v1.13 and became enabled by default in v1.20+ # api-server: http://apiserver.foo.cluster aliases: [foo.alias] groups: [foo] diff --git a/client/azure.go b/client/azure.go index 3c3501a..b84678b 100644 --- a/client/azure.go +++ b/client/azure.go @@ -68,6 +68,12 @@ func (ac *AzureConfig) ValidateConfig() error { if ac.RedirectURI == "" { return errors.New("oauth2 redirect-uri is required for azure targets") } + + for name, target := range ac.Targets { + if target.UseGKEClientConfig && target.APIServer == "" { + return fmt.Errorf("%s: use-gke-clientconfig:true requires api-server to be set", name) + } + } return nil } @@ -146,12 +152,32 @@ func (r *azureRetriever) RetrieveClusterDetailsAndAuthTokens(target Target) (*Ta var apiServerURL, apiServerCA string - if target.ShouldFetchCAFromAPIServer() { + if target.ShouldConfigureForGKE() { + tlsClient, err := web.NewTLSClient() + if err != nil { + return nil, fmt.Errorf("unable to create TLS client: %w", err) + } + req, err := createKubePublicRequest(target.APIServer(), "apis/authentication.gke.io/v2alpha1", "clientconfigs", "default") + if err != nil { + return nil, fmt.Errorf("unable to create API Server request for OIDC ClientConfig: %w", err) + } + resp, err := tlsClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to retrieve OIDC ClientConfig from API Server endpoint: %w", err) + } + clientConfig, err := r.consumeClientConfigResponse(resp) + if err != nil { + return nil, err + } + apiServerURL = clientConfig.Spec.Server + apiServerCA = clientConfig.Spec.CaCertBase64 + + } else if target.ShouldFetchCAFromAPIServer() { tlsClient, err := web.NewTLSClient() if err != nil { return nil, fmt.Errorf("unable to create TLS client: %w", err) } - req, err := createCAConfigMapRequest(target.APIServer()) + req, err := createKubePublicRequest(target.APIServer(), "api/v1", "configmaps", "kube-root-ca.crt") if err != nil { return nil, fmt.Errorf("unable to create API Server request for CA ConfigMap: %w", err) } @@ -195,6 +221,31 @@ func (r *azureRetriever) RetrieveClusterDetailsAndAuthTokens(target Target) (*Ta }, nil } +type clientConfig struct { + Spec clientConfigSpec `json:"spec"` +} +type clientConfigSpec struct { + Server string `json:"server"` + CaCertBase64 string `json:"certificateAuthorityData"` +} + +func (r *azureRetriever) consumeClientConfigResponse(response *http.Response) (*clientConfig, error) { + if response.StatusCode == http.StatusOK { + data, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, fmt.Errorf("failed to read ClientConfig response from API Server: %w", err) + } + defer response.Body.Close() + var clientConfig = &clientConfig{} + err = json.Unmarshal(data, clientConfig) + if err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + return clientConfig, nil + } + return nil, fmt.Errorf("error fetching ClientConfig from API Server: %s", response.Status) +} + type configMap struct { Data configMapData `json:"data"` } diff --git a/client/config.go b/client/config.go index db03187..f371c54 100644 --- a/client/config.go +++ b/client/config.go @@ -38,6 +38,10 @@ type TargetEntry struct { // APIServer is the address of the API server (hostname:port). // +optional APIServer string `yaml:"api-server,omitempty"` + // UseGKEClientConfig true if Osprey should fetch the CA cert and server URL from the + //kube-public/ClientConfig resource provided by the OIDC Identity Service in GKE clusters. + // +optional + UseGKEClientConfig bool `yaml:"use-gke-clientconfig,omitempty"` // CertificateAuthority is the path to a cert file for the certificate authority. // +optional CertificateAuthority string `yaml:"certificate-authority,omitempty"` diff --git a/client/osprey.go b/client/osprey.go index c0cd05c..7eab005 100644 --- a/client/osprey.go +++ b/client/osprey.go @@ -169,11 +169,11 @@ func createClusterInfoRequest(host string) (*http.Request, error) { return req, nil } -func createCAConfigMapRequest(host string) (*http.Request, error) { - url := fmt.Sprintf("%s/api/v1/namespaces/kube-public/configmaps/kube-root-ca.crt", host) +func createKubePublicRequest(host, api, kind, name string) (*http.Request, error) { + url := fmt.Sprintf("%s/%s/namespaces/kube-public/%s/%s", host, api, kind, name) req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { - return nil, fmt.Errorf("unable to create CA ConfigMap request: %w", err) + return nil, fmt.Errorf("unable to create request for %s: %w", url, err) } req.Header.Add("Accept", "application/json") diff --git a/client/target.go b/client/target.go index ab84447..fb2c214 100644 --- a/client/target.go +++ b/client/target.go @@ -37,6 +37,13 @@ func (m *Target) APIServer() string { return m.targetEntry.APIServer } +// ShouldConfigureForGKE returns true iff the API server URL and CA +// should be fetched from the kube-public ClientConfig provided by GKE clusters +// instead of the other methods (e.g. inline in Osprey config file or from Osprey server) +func (m *Target) ShouldConfigureForGKE() bool { + return m.targetEntry.UseGKEClientConfig +} + // ShouldFetchCAFromAPIServer returns true iff the CA should be fetched from the kube-public ConfigMap // instead of the other methods (e.g. inline in Osprey config file or from Osprey server) func (m *Target) ShouldFetchCAFromAPIServer() bool { diff --git a/e2e/apiservertest/server.go b/e2e/apiservertest/server.go index b33fe39..354b6ed 100644 --- a/e2e/apiservertest/server.go +++ b/e2e/apiservertest/server.go @@ -2,13 +2,16 @@ package apiservertest import ( "context" + "encoding/base64" "fmt" "net/http" + "strings" log "github.com/sirupsen/logrus" ) const rootCaRequestPath = "/api/v1/namespaces/kube-public/configmaps/kube-root-ca.crt" +const clientConfigRequestPath = "/apis/authentication.gke.io/v2alpha1/namespaces/kube-public/clientconfigs/default" // Server holds the interface to a mocked API server type Server interface { @@ -44,6 +47,7 @@ func setup(m *mockAPIServer) *http.Server { func initialiseRequestStates() map[string]int { endpoints := []string{ rootCaRequestPath, + clientConfigRequestPath, } requestStates := make(map[string]int) @@ -68,6 +72,7 @@ func Start(host string, port int32) (Server, error) { } server.mux.Handle(rootCaRequestPath, handleRootCaRequest(server)) + server.mux.Handle(clientConfigRequestPath, handleClientConfigRequest(server)) go func() { if err := server.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { @@ -91,10 +96,99 @@ func handleRootCaRequest(m *mockAPIServer) http.HandlerFunc { } } +func handleClientConfigRequest(m *mockAPIServer) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + _, _ = w.Write([]byte(clientConfigResponse)) + m.requestCount[r.URL.Path]++ + } +} + const ( - // CaCertIdentifyingPortion a part of the CA to check when asserting that this particular CA was fetched - CaCertIdentifyingPortion = "MIIGhjCCBW6gAwIBAgITZgAEN7n0RPnqTqxkKAABAAQ3uTANBgkqhkiG9w0BAQsF" - caConfigMapResponse = ` + // CaCert1Pem is used in the kube-root-ca.crt ConfigMap response + CaCert1Pem = `-----BEGIN CERTIFICATE----- +MIIGhjCCBW6gAwIBAgITZgAEN7n0RPnqTqxkKAABAAQ3uTANBgkqhkiG9w0BAQsF +ADBNMRMwEQYKCZImiZPyLGQBGRYDY29tMRUwEwYKCZImiZPyLGQBGRYFYnNreWIx +HzAdBgNVBAMTFk5FVy1CU0tZQi1JU1NVSU5HLUNBMDEwHhcNMjAxMDEyMDkxNDU3 +WhcNMjExMTE0MDkxNDU3WjB0MQswCQYDVQQGEwJHQjESMBAGA1UECBMJTWlkZGxl +c2V4MRIwEAYDVQQHEwlJc2xld29ydGgxEDAOBgNVBAoTB1NLWSBQTEMxDjAMBgNV +BAsTBUdUVkRQMRswGQYDVQQDExJzYW5kZnVuLmNvc21pYy5za3kwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAAET6KrNlTQAXPvpU644VliPHBWu6CFmE +ivK6Bm1WMCZPhD/Zarsl+mXKW594KJDoVaA+DMzwAo/hYnHWoV5wzSPdJb76OI5k +UmBQhYKwr/JqPp/Fz0cTbnG5WYbot/8NQjD6b1yzQq+tiB2OFRoAVcBrlIgRZCwE +EI2QrLx+xJVGFaPQHSyzAW7ym5Qy/E1oxK2inc3iRYKOjwaqJl1DOdPhY67kmvv6 +d4TsI9zP/MYsLW/ndD+mwWXQiEDVStYHhr33447DSKb7ese+202U10zd8XjkPr+T +91XuiqyTmJ23TK1YznsNvUxVXHjWPmCIzZQCf05gnr15j1l74V9JAgMBAAGjggM2 +MIIDMjALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwNwYDVR0RBDAw +LoIUKi5zYW5kZnVuLmNvc21pYy5za3mCFioucy5zYW5kZnVuLmNvc21pYy5za3kw +HQYDVR0OBBYEFA/c0xCQTCuHCYtvo+dE3UbreIbiMB8GA1UdIwQYMBaAFL5qnAMG +DIL0CNps8PhIXXgn7fzsMIIBGwYDVR0fBIIBEjCCAQ4wggEKoIIBBqCCAQKGgbxs +ZGFwOi8vL0NOPU5FVy1CU0tZQi1JU1NVSU5HLUNBMDEsQ049V1BDQUkwMTAsQ049 +Q0RQLENOPVB1YmxpYyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNv +bmZpZ3VyYXRpb24sREM9YnNreWIsREM9Y29tP2NlcnRpZmljYXRlUmV2b2NhdGlv +bkxpc3Q/YmFzZT9vYmplY3RDbGFzcz1jUkxEaXN0cmlidXRpb25Qb2ludIZBaHR0 +cDovL2NlcnRpZmljYXRlcy5ic2t5Yi5jb20vQ2VydERhdGEvTkVXLUJTS1lCLUlT +U1VJTkctQ0EwMS5jcmwwggEaBggrBgEFBQcBAQSCAQwwggEIMIGzBggrBgEFBQcw +AoaBpmxkYXA6Ly8vQ049TkVXLUJTS1lCLUlTU1VJTkctQ0EwMSxDTj1BSUEsQ049 +UHVibGljJTIwS2V5JTIwU2VydmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJh +dGlvbixEQz1ic2t5YixEQz1jb20/Y0FDZXJ0aWZpY2F0ZT9iYXNlP29iamVjdENs +YXNzPWNlcnRpZmljYXRpb25BdXRob3JpdHkwUAYIKwYBBQUHMAKGRGh0dHA6Ly9j +ZXJ0aWZpY2F0ZXMuYnNreWIuY29tL0NlcnREYXRhL05FVy1CU0tZQi1JU1NVSU5H +LUNBMDEoMSkuY3J0MDsGCSsGAQQBgjcVBwQuMCwGJCsGAQQBgjcVCIec8CaBi9Zk +h5GLCK/lB4a83iQYwoEHhsnQcAIBZAIBCjAbBgkrBgEEAYI3FQoEDjAMMAoGCCsG +AQUFBwMBMA0GCSqGSIb3DQEBCwUAA4IBAQAWfuUY1TgWvHR7agr/zv3NzHrQ+NqI +ITDzLyCDwo2511fhuMYl5uAylp2uCQfwTVbMHY3Uktd1VcHFVzrHCvJpzrP+9sFw +Q/paDzWc3i+wtffFpMZD9rzy4C+oYQLM7LGjg1nGWPrseM4iRt0ImH1zbyiNWOUM +/EcC/T3lENmpLH5DHNF1C/wY1NBqiOs4Hqcwtc1rewkX+9f1vuX3m88r9QrJqDd1 +f5OJYejZW0lv8BkA0lPcHGvsBdNaeV6mV3EJ+hu8lo5GVGw4cF2+88wNXccV2d3V +ufyNNGlrVt9iS/qRE/Uo4iluGwg/QElvnY+hgK4fVRFU0fKdwbNQgaiF +-----END CERTIFICATE-----` + + // CaCert2Pem is used in the ClientConfig response + CaCert2Pem = `-----BEGIN CERTIFICATE----- +MIIGbDCCBVSgAwIBAgITZgAFRUo0agut5RU9lgABAAVFSjANBgkqhkiG9w0BAQsF +ADBNMRMwEQYKCZImiZPyLGQBGRYDY29tMRUwEwYKCZImiZPyLGQBGRYFYnNreWIx +HzAdBgNVBAMTFk5FVy1CU0tZQi1JU1NVSU5HLUNBMDEwHhcNMjEwOTIyMTQxMTIz +WhcNMjIxMDI1MTQxMTIzWjB0MQswCQYDVQQGEwJHQjESMBAGA1UECBMJTWlkZGxl +c2V4MRIwEAYDVQQHEwlJc2xld29ydGgxEDAOBgNVBAoTB1NLWSBQTEMxDjAMBgNV +BAsTBUdUVkRQMRswGQYDVQQDExJzYW5kbmZ0LmNvc21pYy5za3kwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDjFQ+ubBMMkT3aPaGWJTcQQgaGjwS1Fbvs +Hm6I6g06euAJ7z1dmZW8JF5/PBSsOh00PyERPHItJVpc44kS56WGcJfs0tKjpQFv +QVfa2mGU0/R4qwnPjYhraJAyU4FiQ2SVzbRzhzWx+vxWXaz9yr9XMly+So3vgUK9 +2DoHnJ0833vkgxjMZEE530KfrGy+bkp8ieIXNA6TiQptLROoGzFJldB8IduAao06 +RNj5ssAYRPjpWgJBg20ya+H0M+CzRECBW+bGYpinKRZZ2xVr2QGCXZFYcExg0P0g +WlKQmbQXNQeYP+djO+908j9WpOnNzns8dFnj3WynMh0gtR7UPtCpAgMBAAGjggMc +MIIDGDALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYDVR0RBBYw +FIISc2FuZG5mdC5jb3NtaWMuc2t5MB0GA1UdDgQWBBQgUUqjKE74yhSsQXg3sDrq +0W3S2zAfBgNVHSMEGDAWgBS+apwDBgyC9AjabPD4SF14J+387DCCARsGA1UdHwSC +ARIwggEOMIIBCqCCAQagggEChoG8bGRhcDovLy9DTj1ORVctQlNLWUItSVNTVUlO +Ry1DQTAxLENOPVdQQ0FJMDEwLENOPUNEUCxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2 +aWNlcyxDTj1TZXJ2aWNlcyxDTj1Db25maWd1cmF0aW9uLERDPWJza3liLERDPWNv +bT9jZXJ0aWZpY2F0ZVJldm9jYXRpb25MaXN0P2Jhc2U/b2JqZWN0Q2xhc3M9Y1JM +RGlzdHJpYnV0aW9uUG9pbnSGQWh0dHA6Ly9jZXJ0aWZpY2F0ZXMuYnNreWIuY29t +L0NlcnREYXRhL05FVy1CU0tZQi1JU1NVSU5HLUNBMDEuY3JsMIIBGgYIKwYBBQUH +AQEEggEMMIIBCDCBswYIKwYBBQUHMAKGgaZsZGFwOi8vL0NOPU5FVy1CU0tZQi1J +U1NVSU5HLUNBMDEsQ049QUlBLENOPVB1YmxpYyUyMEtleSUyMFNlcnZpY2VzLENO +PVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9YnNreWIsREM9Y29tP2NBQ2Vy +dGlmaWNhdGU/YmFzZT9vYmplY3RDbGFzcz1jZXJ0aWZpY2F0aW9uQXV0aG9yaXR5 +MFAGCCsGAQUFBzAChkRodHRwOi8vY2VydGlmaWNhdGVzLmJza3liLmNvbS9DZXJ0 +RGF0YS9ORVctQlNLWUItSVNTVUlORy1DQTAxKDEpLmNydDA7BgkrBgEEAYI3FQcE +LjAsBiQrBgEEAYI3FQiHnPAmgYvWZIeRiwiv5QeGvN4kGMKBB4bJ0HACAWQCAQww +GwYJKwYBBAGCNxUKBA4wDDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA +JkAcuZywpTzMIqs3rfehWUdFObDlsPqv14J1EITWQysYYxUy3QUveJwRsOsI4/TL +X4nivEKvaoCxrISmMmo4Yg6CQCk1VAREW/m2EfYKT+jxQX/sWpdyf/hFAJsGg5Qx +lOFkBOG0wk0Qf+grzFyiWXY5i7aTqhvd3o3setGXFYGVjmEveB05Aj4GkAREakpt +gjBR4IB3LWa+TzdBnYEp6OKYw3bWag6HLS/yndq1YJnuH9ksaJk1vx5HOeocc7Iv +3AqjDqzkRXbC86vP9LJFZAzA4VhpSi3g276CTXSDVZwXV9CswIf3nmCNcKjenU++ +lyVgLSFHid1LbnPN/klDPw== +-----END CERTIFICATE-----` + + // InternalAPIServerURL is the API server URL returned in the GKE ClientConfig resource, representing the Envoy proxy for OIDC requests + InternalAPIServerURL = "https://10.10.10.10:443" +) + +var ( + caConfigMapResponse = ` { "kind": "ConfigMap", "apiVersion": "v1", @@ -103,7 +197,17 @@ const ( "namespace": "kube-public" }, "data": { - "ca.crt": "-----BEGIN CERTIFICATE-----\n` + CaCertIdentifyingPortion + `\nADBNMRMwEQYKCZImiZPyLGQBGRYDY29tMRUwEwYKCZImiZPyLGQBGRYFYnNreWIx\nHzAdBgNVBAMTFk5FVy1CU0tZQi1JU1NVSU5HLUNBMDEwHhcNMjAxMDEyMDkxNDU3\nWhcNMjExMTE0MDkxNDU3WjB0MQswCQYDVQQGEwJHQjESMBAGA1UECBMJTWlkZGxl\nc2V4MRIwEAYDVQQHEwlJc2xld29ydGgxEDAOBgNVBAoTB1NLWSBQTEMxDjAMBgNV\nBAsTBUdUVkRQMRswGQYDVQQDExJzYW5kZnVuLmNvc21pYy5za3kwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAAET6KrNlTQAXPvpU644VliPHBWu6CFmE\nivK6Bm1WMCZPhD/Zarsl+mXKW594KJDoVaA+DMzwAo/hYnHWoV5wzSPdJb76OI5k\nUmBQhYKwr/JqPp/Fz0cTbnG5WYbot/8NQjD6b1yzQq+tiB2OFRoAVcBrlIgRZCwE\nEI2QrLx+xJVGFaPQHSyzAW7ym5Qy/E1oxK2inc3iRYKOjwaqJl1DOdPhY67kmvv6\nd4TsI9zP/MYsLW/ndD+mwWXQiEDVStYHhr33447DSKb7ese+202U10zd8XjkPr+T\n91XuiqyTmJ23TK1YznsNvUxVXHjWPmCIzZQCf05gnr15j1l74V9JAgMBAAGjggM2\nMIIDMjALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwNwYDVR0RBDAw\nLoIUKi5zYW5kZnVuLmNvc21pYy5za3mCFioucy5zYW5kZnVuLmNvc21pYy5za3kw\nHQYDVR0OBBYEFA/c0xCQTCuHCYtvo+dE3UbreIbiMB8GA1UdIwQYMBaAFL5qnAMG\nDIL0CNps8PhIXXgn7fzsMIIBGwYDVR0fBIIBEjCCAQ4wggEKoIIBBqCCAQKGgbxs\nZGFwOi8vL0NOPU5FVy1CU0tZQi1JU1NVSU5HLUNBMDEsQ049V1BDQUkwMTAsQ049\nQ0RQLENOPVB1YmxpYyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNv\nbmZpZ3VyYXRpb24sREM9YnNreWIsREM9Y29tP2NlcnRpZmljYXRlUmV2b2NhdGlv\nbkxpc3Q/YmFzZT9vYmplY3RDbGFzcz1jUkxEaXN0cmlidXRpb25Qb2ludIZBaHR0\ncDovL2NlcnRpZmljYXRlcy5ic2t5Yi5jb20vQ2VydERhdGEvTkVXLUJTS1lCLUlT\nU1VJTkctQ0EwMS5jcmwwggEaBggrBgEFBQcBAQSCAQwwggEIMIGzBggrBgEFBQcw\nAoaBpmxkYXA6Ly8vQ049TkVXLUJTS1lCLUlTU1VJTkctQ0EwMSxDTj1BSUEsQ049\nUHVibGljJTIwS2V5JTIwU2VydmljZXMsQ049U2VydmljZXMsQ049Q29uZmlndXJh\ndGlvbixEQz1ic2t5YixEQz1jb20/Y0FDZXJ0aWZpY2F0ZT9iYXNlP29iamVjdENs\nYXNzPWNlcnRpZmljYXRpb25BdXRob3JpdHkwUAYIKwYBBQUHMAKGRGh0dHA6Ly9j\nZXJ0aWZpY2F0ZXMuYnNreWIuY29tL0NlcnREYXRhL05FVy1CU0tZQi1JU1NVSU5H\nLUNBMDEoMSkuY3J0MDsGCSsGAQQBgjcVBwQuMCwGJCsGAQQBgjcVCIec8CaBi9Zk\nh5GLCK/lB4a83iQYwoEHhsnQcAIBZAIBCjAbBgkrBgEEAYI3FQoEDjAMMAoGCCsG\nAQUFBwMBMA0GCSqGSIb3DQEBCwUAA4IBAQAWfuUY1TgWvHR7agr/zv3NzHrQ+NqI\nITDzLyCDwo2511fhuMYl5uAylp2uCQfwTVbMHY3Uktd1VcHFVzrHCvJpzrP+9sFw\nQ/paDzWc3i+wtffFpMZD9rzy4C+oYQLM7LGjg1nGWPrseM4iRt0ImH1zbyiNWOUM\n/EcC/T3lENmpLH5DHNF1C/wY1NBqiOs4Hqcwtc1rewkX+9f1vuX3m88r9QrJqDd1\nf5OJYejZW0lv8BkA0lPcHGvsBdNaeV6mV3EJ+hu8lo5GVGw4cF2+88wNXccV2d3V\nufyNNGlrVt9iS/qRE/Uo4iluGwg/QElvnY+hgK4fVRFU0fKdwbNQgaiF\n-----END CERTIFICATE-----" + "ca.crt": "` + strings.ReplaceAll(CaCert1Pem, "\n", `\n`) + `" + } +}` + // This ClientConfig response contains only the pertinent parts + clientConfigResponse = ` +{ + "apiVersion": "authentication.gke.io/v2alpha1", + "kind": "ClientConfig", + "spec": { + "certificateAuthorityData": "` + base64.StdEncoding.EncodeToString([]byte(CaCert2Pem)) + `", + "server": "` + InternalAPIServerURL + `" } }` ) diff --git a/e2e/e2e_suite_test.go b/e2e/e2e_suite_test.go index 9c5db5e..b964a45 100644 --- a/e2e/e2e_suite_test.go +++ b/e2e/e2e_suite_test.go @@ -49,15 +49,16 @@ var ( testDir string // Suite variables modifiable per test scenario - err error - environmentsToUse map[string][]string - targetedOspreys []*ospreytest.TestOsprey - ospreyconfig *ospreytest.TestConfig - ospreyconfigFlag string - defaultGroup string - targetGroup string - targetGroupFlag string - apiServerURL string + err error + environmentsToUse map[string][]string + targetedOspreys []*ospreytest.TestOsprey + ospreyconfig *ospreytest.TestConfig + ospreyconfigFlag string + defaultGroup string + targetGroup string + targetGroupFlag string + apiServerURL string + useGKEClientConfig bool ) var _ = BeforeSuite(func() { @@ -101,8 +102,8 @@ var _ = AfterSuite(func() { os.RemoveAll(testDir) }) -func setupClientForEnvironments(providerName string, envs map[string][]string, clientID, apiServerURL string) { - ospreyconfig, err = ospreytest.BuildConfig(testDir, providerName, defaultGroup, envs, ospreys, clientID, apiServerURL) +func setupClientForEnvironments(providerName string, envs map[string][]string, clientID, apiServerURL string, useGKEClientConfig bool) { + ospreyconfig, err = ospreytest.BuildConfig(testDir, providerName, defaultGroup, envs, ospreys, clientID, apiServerURL, useGKEClientConfig) Expect(err).To(BeNil(), "Creates the osprey config with groups") ospreyconfigFlag = "--ospreyconfig=" + ospreyconfig.ConfigFile @@ -122,6 +123,7 @@ func resetDefaults() { targetGroup = "" targetGroupFlag = "" apiServerURL = "" + useGKEClientConfig = false } func cleanup() { diff --git a/e2e/login_test.go b/e2e/login_test.go index a1ea625..c76cea8 100644 --- a/e2e/login_test.go +++ b/e2e/login_test.go @@ -20,7 +20,7 @@ var _ = Describe("Login", func() { }) JustBeforeEach(func() { - setupClientForEnvironments(ospreyProviderName, environmentsToUse, "", "") + setupClientForEnvironments(ospreyProviderName, environmentsToUse, "", "", false) login = Login("user", "login", ospreyconfigFlag, targetGroupFlag, "--disable-browser-popup") }) @@ -43,7 +43,7 @@ var _ = Describe("Login", func() { }) It("logs in with certificate-authority-data", func() { - caDataConfig, err := BuildCADataConfig(testDir, ospreyProviderName, ospreys, true, "", "", "") + caDataConfig, err := BuildCADataConfig(testDir, ospreyProviderName, ospreys, true, "", "", "", false) Expect(err).To(BeNil(), "Creates the osprey config") caDataConfigFlag := "--ospreyconfig=" + caDataConfig.ConfigFile caDataLogin := Login("user", "login", caDataConfigFlag) @@ -52,7 +52,7 @@ var _ = Describe("Login", func() { }) It("logs in overriding certificate-authority with certificate-authority-data", func() { - caDataConfig, err := BuildCADataConfig(testDir, ospreyProviderName, ospreys, true, dexes[0].DexCA, "", "") + caDataConfig, err := BuildCADataConfig(testDir, ospreyProviderName, ospreys, true, dexes[0].DexCA, "", "", false) Expect(err).To(BeNil(), "Creates the osprey config") caDataConfigFlag := "--ospreyconfig=" + caDataConfig.ConfigFile caDataLogin := Login("user", "login", caDataConfigFlag) @@ -62,7 +62,7 @@ var _ = Describe("Login", func() { It("does not allow fetching CA from API Server for Osprey targets", func() { caDataConfig, err := BuildCADataConfig(testDir, ospreyProviderName, ospreys, true, - dexes[0].DexCA, "", fmt.Sprintf("http://localhost:%d", apiServerPort)) + dexes[0].DexCA, "", fmt.Sprintf("http://localhost:%d", apiServerPort), false) Expect(err).To(BeNil(), "Creates the osprey config") caDataConfigFlag := "--ospreyconfig=" + caDataConfig.ConfigFile caDataLogin := Login("user", "login", caDataConfigFlag) diff --git a/e2e/logout_test.go b/e2e/logout_test.go index 4e22944..e60906c 100644 --- a/e2e/logout_test.go +++ b/e2e/logout_test.go @@ -20,7 +20,7 @@ var _ = Describe("Logout", func() { }) JustBeforeEach(func() { - setupClientForEnvironments(ospreyProviderName, environmentsToUse, "", "") + setupClientForEnvironments(ospreyProviderName, environmentsToUse, "", "", false) login = Login("user", "login", ospreyconfigFlag, targetGroupFlag) logout = Client("user", "logout", ospreyconfigFlag, targetGroupFlag) diff --git a/e2e/oidc_login_test.go b/e2e/oidc_login_test.go index 7c3b18c..be2c5e3 100644 --- a/e2e/oidc_login_test.go +++ b/e2e/oidc_login_test.go @@ -37,7 +37,7 @@ var _ = Describe("Login with a cloud provider", func() { }) JustBeforeEach(func() { - setupClientForEnvironments(azureProviderName, environmentsToUse, oidcClientID, apiServerURL) + setupClientForEnvironments(azureProviderName, environmentsToUse, oidcClientID, apiServerURL, useGKEClientConfig) userLoginArgs = []string{"user", "login", ospreyconfigFlag, "--disable-browser-popup"} }) @@ -96,13 +96,52 @@ var _ = Describe("Login with a cloud provider", func() { login.AssertSuccess() Expect(apiTestServer.RequestCount("/api/v1/namespaces/kube-public/configmaps/kube-root-ca.crt")).To(Equal(1)) kubeconfig := getKubeConfig() - Expect(kubeconfig.Clusters["kubectl.local"].CertificateAuthorityData).To(ContainSubstring(apiservertest.CaCertIdentifyingPortion)) + Expect(kubeconfig.Clusters["kubectl.local"].CertificateAuthorityData).To(Equal([]byte(apiservertest.CaCert1Pem))) + }) + }) + }) + + Describe("fetches the API server URL from the requested location", func() { + Describe("with use-gke-clientconfig provided", func() { + BeforeEach(func() { + apiServerURL = fmt.Sprintf("http://localhost:%d", apiServerPort) + useGKEClientConfig = true + }) + It("URL should be fetched from GKE's ClientConfig resource", func() { + login := loginCommand(ospreyBinary, userLoginArgs...) + + _, err := doOIDCMockRequest("/authorize", oidcClientID, oidcRedirectURI, ospreyState, []string{"api://some-dummy-scope"}) + Expect(err).NotTo(HaveOccurred()) + + login.AssertSuccess() + Expect(apiTestServer.RequestCount("/apis/authentication.gke.io/v2alpha1/namespaces/kube-public/clientconfigs/default")).To(Equal(1)) + kubeconfig := getKubeConfig() + Expect(kubeconfig.Clusters["kubectl.local"].Server).To(Equal(apiservertest.InternalAPIServerURL)) + Expect(kubeconfig.Clusters["kubectl.local"].CertificateAuthorityData).To(Equal([]byte(apiservertest.CaCert2Pem))) + }) + }) + Describe("without use-gke-clientconfig provided", func() { + BeforeEach(func() { + apiServerURL = fmt.Sprintf("http://localhost:%d", apiServerPort) + useGKEClientConfig = false + }) + It("URL should be the same as the configured api-server field", func() { + login := loginCommand(ospreyBinary, userLoginArgs...) + + _, err := doOIDCMockRequest("/authorize", oidcClientID, oidcRedirectURI, ospreyState, []string{"api://some-dummy-scope"}) + Expect(err).NotTo(HaveOccurred()) + + login.AssertSuccess() + Expect(apiTestServer.RequestCount("/apis/authentication.gke.io/v2alpha1/namespaces/kube-public/clientconfigs/default")).To(Equal(0)) + kubeconfig := getKubeConfig() + Expect(kubeconfig.Clusters["kubectl.local"].Server).To(Equal(apiServerURL)) + Expect(kubeconfig.Clusters["kubectl.local"].CertificateAuthorityData).To(Equal([]byte(apiservertest.CaCert1Pem))) }) }) }) It("provides the same JWT token for multiple targets in group for the same provider", func() { - setupClientForEnvironments(azureProviderName, map[string][]string{"dev": {"development"}, "stage": {"development"}}, oidcClientID, "") + setupClientForEnvironments(azureProviderName, map[string][]string{"dev": {"development"}, "stage": {"development"}}, oidcClientID, "", false) targetGroupArgs := append(userLoginArgs, "--group=development") login := loginCommand(ospreyBinary, targetGroupArgs...) @@ -141,7 +180,7 @@ var _ = Describe("Login with a cloud provider", func() { }) It("provides the same JWT token for multiple targets in group for the same provider", func() { - setupClientForEnvironments(azureProviderName, map[string][]string{"dev": {"development"}, "stage": {"development"}}, oidcClientID, "") + setupClientForEnvironments(azureProviderName, map[string][]string{"dev": {"development"}, "stage": {"development"}}, oidcClientID, "", false) targetGroupArgs := append(userLoginArgs, "--group=development", "--use-device-code") login := loginCommand(ospreyBinary, targetGroupArgs...) @@ -157,7 +196,7 @@ var _ = Describe("Login with a cloud provider", func() { }) It("Polls the token endpoint at server specified intervals when token status is pending", func() { - setupClientForEnvironments(azureProviderName, environmentsToUse, "pending_client_id", "") + setupClientForEnvironments(azureProviderName, environmentsToUse, "pending_client_id", "", false) useDeviceCodeArgs := append(userLoginArgs, "--use-device-code") login := loginCommand(ospreyBinary, useDeviceCodeArgs...) @@ -170,7 +209,7 @@ var _ = Describe("Login with a cloud provider", func() { }) It("Handles client id is not authorised error code", func() { - setupClientForEnvironments(azureProviderName, environmentsToUse, "bad_verification_client_id", "") + setupClientForEnvironments(azureProviderName, environmentsToUse, "bad_verification_client_id", "", false) useDeviceCodeArgs := append(userLoginArgs, "--use-device-code") login := loginCommand(ospreyBinary, useDeviceCodeArgs...) @@ -181,7 +220,7 @@ var _ = Describe("Login with a cloud provider", func() { }) It("Handles device code expired error code", func() { - setupClientForEnvironments(azureProviderName, environmentsToUse, "expired_client_id", "") + setupClientForEnvironments(azureProviderName, environmentsToUse, "expired_client_id", "", false) useDeviceCodeArgs := append(userLoginArgs, "--use-device-code") login := loginCommand(ospreyBinary, useDeviceCodeArgs...) @@ -194,7 +233,7 @@ var _ = Describe("Login with a cloud provider", func() { Context("Specifiying the --login-timeout flag", func() { It("logs in successfully if the flow is complete before the timeout", func() { - setupClientForEnvironments(azureProviderName, environmentsToUse, "login_timeout_exceeded_client_id", "") + setupClientForEnvironments(azureProviderName, environmentsToUse, "login_timeout_exceeded_client_id", "", false) timeoutArgs := append(userLoginArgs, "--login-timeout=20s") login := loginCommand(ospreyBinary, timeoutArgs...) @@ -205,7 +244,7 @@ var _ = Describe("Login with a cloud provider", func() { }) It("callback flow times out if not logged in within the stipulated time", func() { - setupClientForEnvironments(azureProviderName, environmentsToUse, "login_timeout_exceeded_client_id", "") + setupClientForEnvironments(azureProviderName, environmentsToUse, "login_timeout_exceeded_client_id", "", false) timeoutArgs := append(userLoginArgs, "--login-timeout=1s") login := loginCommand(ospreyBinary, timeoutArgs...) @@ -214,7 +253,7 @@ var _ = Describe("Login with a cloud provider", func() { }) It("device-code flow times out if not logged in within the stipulated time", func() { - setupClientForEnvironments(azureProviderName, environmentsToUse, "login_timeout_exceeded_client_id", "") + setupClientForEnvironments(azureProviderName, environmentsToUse, "login_timeout_exceeded_client_id", "", false) deviceCodeTimeoutArgs := append(userLoginArgs, "--use-device-code=true", "--login-timeout=1s") login := loginCommand(ospreyBinary, deviceCodeTimeoutArgs...) diff --git a/e2e/ospreytest/server.go b/e2e/ospreytest/server.go index f6cf644..69272ef 100644 --- a/e2e/ospreytest/server.go +++ b/e2e/ospreytest/server.go @@ -117,18 +117,18 @@ func Stop(server *TestOsprey) error { // BuildConfig creates an ospreyconfig file using the groups provided for the targets. // It uses testDir as the home for the .kube and .osprey folders. func BuildConfig(testDir, providerName, defaultGroup string, targetGroups map[string][]string, - servers []*TestOsprey, clientID, apiServerURL string) (*TestConfig, error) { + servers []*TestOsprey, clientID, apiServerURL string, useGKEClientConfig bool) (*TestConfig, error) { return BuildFullConfig(testDir, providerName, defaultGroup, targetGroups, servers, - false, "", clientID, apiServerURL) + false, "", clientID, apiServerURL, useGKEClientConfig) } // BuildCADataConfig creates an ospreyconfig file with as many targets as servers are provided. // It uses testDir as the home for the .kube and .osprey folders. // It also base64 encodes the CA data instead of using the file path. func BuildCADataConfig(testDir, providerName string, servers []*TestOsprey, - caData bool, caPath, clientID, apiServerURL string) (*TestConfig, error) { + caData bool, caPath, clientID, apiServerURL string, useGKEClientConfig bool) (*TestConfig, error) { return BuildFullConfig(testDir, providerName, "", map[string][]string{}, servers, - caData, caPath, clientID, apiServerURL) + caData, caPath, clientID, apiServerURL, useGKEClientConfig) } // BuildFullConfig creates an ospreyconfig file with as many targets as servers are provided. The targets will contain @@ -137,7 +137,7 @@ func BuildCADataConfig(testDir, providerName string, servers []*TestOsprey, // If caData is true, it base64 encodes the CA data instead of using the file path. func BuildFullConfig(testDir, providerName, defaultGroup string, targetGroups map[string][]string, servers []*TestOsprey, - caData bool, caPath, clientID, apiServerURL string) (*TestConfig, error) { + caData bool, caPath, clientID, apiServerURL string, useGKEClientConfig bool) (*TestConfig, error) { config := client.NewConfig() config.Kubeconfig = fmt.Sprintf("%s/.kube/config", testDir) ospreyconfigFile := fmt.Sprintf("%s/.osprey/config", testDir) @@ -158,6 +158,8 @@ func BuildFullConfig(testDir, providerName, defaultGroup string, Aliases: []string{osprey.OspreyconfigAliasName()}, } + target.UseGKEClientConfig = useGKEClientConfig + shouldFetchCAFromAPIServer := apiServerURL != "" if shouldFetchCAFromAPIServer { target.APIServer = apiServerURL diff --git a/e2e/targets_test.go b/e2e/targets_test.go index 0834d80..993de2d 100644 --- a/e2e/targets_test.go +++ b/e2e/targets_test.go @@ -28,7 +28,7 @@ var _ = Describe("Targets", func() { }) JustBeforeEach(func() { - setupClientForEnvironments(ospreyProviderName, environmentsToUse, "", "") + setupClientForEnvironments(ospreyProviderName, environmentsToUse, "", "", false) targets = Client("config", "targets", ospreyconfigFlag, targetGroupFlag, byGroupsFlag, listGroupsFlag) }) diff --git a/e2e/user_test.go b/e2e/user_test.go index 35438c1..60a1a98 100644 --- a/e2e/user_test.go +++ b/e2e/user_test.go @@ -19,7 +19,7 @@ var _ = Describe("User", func() { }) JustBeforeEach(func() { - setupClientForEnvironments(ospreyProviderName, environmentsToUse, "", "") + setupClientForEnvironments(ospreyProviderName, environmentsToUse, "", "", false) user = Client("user", ospreyconfigFlag, targetGroupFlag) login = Login("user", "login", ospreyconfigFlag, targetGroupFlag)