github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/gangway/client/google/google.go (about) 1 /* 2 Copyright 2023 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package google 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "os" 24 25 "golang.org/x/oauth2" 26 googleOAuth "golang.org/x/oauth2/google" 27 "google.golang.org/grpc" 28 "google.golang.org/grpc/credentials" 29 "google.golang.org/grpc/credentials/insecure" 30 "google.golang.org/grpc/metadata" 31 32 pb "sigs.k8s.io/prow/pkg/gangway" 33 gangwayClient "sigs.k8s.io/prow/pkg/gangway/client" 34 ) 35 36 // This is the client library for Go clients that need to access to the Prow 37 // API, aka Gangway, when Gangway is deployed in a GKE cluster and integrated 38 // with Cloud Endpoints. 39 // 40 // Go clients need to always append 2 things to the metadata of a gRPC call: 41 // 42 // 1. The JWT (authentication) token (to make the call identify itself as 43 // an allowlisted client in our api_config_auth.yaml configuration for Cloud 44 // Endpoints [1]), and 45 // 46 // 2. The API key (that is generated by the client's GCP Project). 47 // 48 // The JWT token is generated from a GCP Service Account key file (JSON). The 49 // API key is generated from the GCP user interface, like this: 50 // https://cloud.google.com/docs/authentication/api-keys#create. 51 // 52 // For us, the clients must supply the service account JSON key file, API key, 53 // audience, and finally the address where Gangway is being served. An example 54 // client application using this library is provided in the Prow codebase under 55 // prow/gangway/example/main.go. 56 // 57 // [1]: https://github.com/GoogleCloudPlatform/golang-samples/blob/e888c56cb843f475db4f79b391be999518e63db4/endpoints/getting-started-grpc/README.md#configuring-authentication-and-authenticating-requests 58 59 type Client struct { 60 // JWT token-based authentication and GCP Project identification. 61 keyBytes []byte 62 audience string 63 tokenSource oauth2.TokenSource 64 65 // apiKey identifies the GCP Project. 66 apiKey string 67 68 addr string 69 conn *grpc.ClientConn 70 71 // Include common client methods as well. 72 gangwayClient.Common 73 } 74 75 // NewFromFile creates a Gangway client from a JSON service account key file and an audience string. 76 func NewFromFile(addr, keyFile, audience, clientPem, apiKey string) (*Client, error) { 77 keyBytes, err := os.ReadFile(keyFile) 78 if err != nil { 79 return nil, fmt.Errorf("Unable to read service account key file %s: %v", keyFile, err) 80 } 81 82 return New(addr, keyBytes, audience, clientPem, apiKey) 83 } 84 85 // New creates a new gRPC client. It does most of the work in NewFromFile(). 86 func New(addr string, keyBytes []byte, audience, clientPem, apiKey string) (*Client, error) { 87 c := Client{} 88 89 creds, err := credentials.NewClientTLSFromFile(clientPem, "") 90 if err != nil { 91 return nil, fmt.Errorf("could not process clientPem credentials: %v", err) 92 } 93 94 c.addr = addr 95 96 conn, err := grpc.Dial(c.addr, grpc.WithTransportCredentials(creds)) 97 if err != nil { 98 return nil, fmt.Errorf("could not connect to %q: %v", c.addr, err) 99 } 100 c.conn = conn 101 c.GRPC = pb.NewProwClient(c.conn) 102 103 if len(audience) == 0 { 104 return nil, errors.New("audience cannot be empty") 105 } 106 107 c.audience = audience 108 109 if len(apiKey) == 0 { 110 return nil, errors.New("apiKey cannot be empty") 111 } 112 113 c.apiKey = apiKey 114 115 if len(keyBytes) == 0 { 116 return nil, errors.New("keyBytes cannot be empty") 117 } 118 119 c.keyBytes = keyBytes 120 121 tokenSource, err := googleOAuth.JWTAccessTokenSourceFromJSON(c.keyBytes, c.audience) 122 if err != nil { 123 return nil, fmt.Errorf("could not create tokenSource: %v", err) 124 } 125 126 c.tokenSource = tokenSource 127 128 return &c, nil 129 } 130 131 // MkToken generates a new JWT token with a 1h TTL. This is apparently a 132 // cheap operation, according to 133 // https://github.com/GoogleCloudPlatform/golang-samples/blob/e7a5459d85661a35c5eb4f0b5759b7b30ac6ff90/endpoints/getting-started-grpc/client/main.go#L81-L88. 134 func (c *Client) MkToken() (string, error) { 135 jwt, err := c.tokenSource.Token() 136 if err != nil { 137 return "", fmt.Errorf("could not generate JSON Web Token: %v", err) 138 } 139 140 return jwt.AccessToken, nil 141 } 142 143 // EmbedCredentials is used to modify a provided context so that it has the the 144 // necessary token and apiKey attached to it in the metadata. 145 func (c *Client) EmbedCredentials(ctx context.Context) (context.Context, error) { 146 ctxWithCreds := metadata.AppendToOutgoingContext(ctx, "x-api-key", c.apiKey) 147 148 token, err := c.MkToken() 149 if err != nil { 150 return ctxWithCreds, err 151 } 152 153 fmt.Printf("using token %q\n", token) 154 155 ctxWithCreds = metadata.AppendToOutgoingContext(ctxWithCreds, "Authorization", fmt.Sprintf("Bearer %s", token)) 156 157 return ctxWithCreds, nil 158 } 159 160 func (c *Client) Close() { 161 c.conn.Close() 162 } 163 164 type ClientInsecure struct { 165 projectNumber string 166 167 addr string 168 conn *grpc.ClientConn 169 170 // Include common client methods as well. 171 gangwayClient.Common 172 } 173 174 func NewInsecure(addr, projectNumber string) (*ClientInsecure, error) { 175 c := ClientInsecure{} 176 177 c.addr = addr 178 c.projectNumber = projectNumber 179 180 // Set up a connection to gangway. 181 conn, err := grpc.Dial(c.addr, grpc.WithTransportCredentials(insecure.NewCredentials())) 182 if err != nil { 183 return nil, fmt.Errorf("could not connect to %q: %v", c.addr, err) 184 } 185 186 c.conn = conn 187 c.GRPC = pb.NewProwClient(c.conn) 188 189 return &c, nil 190 } 191 192 func (c *ClientInsecure) EmbedProjectNumber(ctx context.Context) context.Context { 193 md := []string{ 194 "x-endpoint-api-consumer-type", "PROJECT", 195 "x-endpoint-api-consumer-number", c.projectNumber, 196 } 197 198 ctx = metadata.NewOutgoingContext( 199 ctx, 200 metadata.Pairs(md...), 201 ) 202 203 return ctx 204 } 205 206 func (c *ClientInsecure) Close() { 207 c.conn.Close() 208 }