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  }