diff --git a/hack/make-rules/update/codegen.sh b/hack/make-rules/update/codegen.sh index 1810e2492475e..81f44690f7e2f 100755 --- a/hack/make-rules/update/codegen.sh +++ b/hack/make-rules/update/codegen.sh @@ -298,6 +298,19 @@ gen-all-proto-stubs(){ -print0 | sort -z) } +gen-gangway-apidescriptorpb-for-cloud-endpoints(){ + echo >&2 "Generating self-describing proto stub (api_descriptor.pb) for gangway.proto" + + "${REPO_ROOT}/_bin/protoc/bin/protoc" \ + "--proto_path=${REPO_ROOT}/_bin/protoc/include/google/protobuf" \ + "--proto_path=${REPO_ROOT}/_bin/protoc/include/googleapis" \ + "--proto_path=${REPO_ROOT}/prow/gangway" \ + --include_imports \ + --include_source_info \ + --descriptor_set_out "${REPO_ROOT}/prow/gangway/api_descriptor.pb" \ + gangway.proto +} + gen-prow-config-documented export GO111MODULE=off @@ -320,3 +333,4 @@ gen-prowjob-crd export GO111MODULE=on gen-all-proto-stubs +gen-gangway-apidescriptorpb-for-cloud-endpoints diff --git a/prow/gangway/api_descriptor.pb b/prow/gangway/api_descriptor.pb new file mode 100644 index 0000000000000..031da2f9ecbc7 Binary files /dev/null and b/prow/gangway/api_descriptor.pb differ diff --git a/prow/gangway/example/main.go b/prow/gangway/example/main.go new file mode 100644 index 0000000000000..1b1bdb080e4ad --- /dev/null +++ b/prow/gangway/example/main.go @@ -0,0 +1,101 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "flag" + "log" + + "google.golang.org/protobuf/encoding/prototext" + pb "k8s.io/test-infra/prow/gangway" + gangwayGoogleClient "k8s.io/test-infra/prow/gangway/google" +) + +var ( + addr = flag.String("addr", "127.0.0.1:50051", "Address of grpc server.") + apiKey = flag.String("api-key", "", "API key.") + audience = flag.String("audience", "", "Audience.") + keyFile = flag.String("key-file", "", "Path to a Google service account key file.") + clientPem = flag.String("client-pem", "", "Path to a client.pem file.") +) + +// Use like this: +// +// go run main.go --key-file=key.json \ +// --audience=SERVICE_NAME.endpoints.PROJECT_NAME.cloud.goog \ +// --addr=12.34.56.78:443 --api-key=API_KEY --client-pem=client.pem \ +// +// +// where is the protobuf (in textpb format) message +// you want to send over. For example, if you want to run the periodic job named +// "foo", use: +// +// 'job_name: "foo", job_execution_type: 1' +// +// as the . The "1" here for "job_execution_type" +// denotes the periodic job type (because this field is an enum, not a string). + +func main() { + flag.Parse() + + jobName := "some-job" + jobExecutionType := pb.JobExecutionType_PERIODIC + + // Set default values. + cjer := pb.CreateJobExecutionRequest{ + JobName: jobName, + JobExecutionType: jobExecutionType, + } + + // Read in string version of a CreateJobExecutionRequest. + if len(flag.Args()) > 0 { + textpb := flag.Arg(0) + if err := prototext.Unmarshal([]byte(textpb), &cjer); err != nil { + log.Fatalf("could not unmarshal textpb %q: %v", textpb, err) + } + } + + log.Printf("creating job execution with %v", &cjer) + + // Create a Prow API gRPC client that's able to authenticate to Gangway (the + // Prow API Server). + prowClient, err := gangwayGoogleClient.NewFromFile(*keyFile, *audience, *apiKey, *addr, *clientPem) + if err != nil { + log.Fatalf("Prow API client creation failed: %v", err) + } + + defer prowClient.Close() + + // Create a Context that has credentials injected inside it. + ctx, err := prowClient.EmbedCredentials(context.Background()) + if err != nil { + log.Fatalf("could not create a context with embedded credentials: %v", err) + } + + // Trigger job! Because this is gRPC it's just a function call. + jobExecution, err := prowClient.GRPC.CreateJobExecution(ctx, &cjer) + if err != nil { + log.Fatalf("could not trigger job: %v", err) + } + + log.Printf("triggered job: %v", jobExecution) + + // FIXME (listx): At this point we have the jobExecution object and it has an `Id` + // field. In order to get the job status, we have to poll + // prowClient.GRPC.GetJobExecution() to get the current job status. +} diff --git a/prow/gangway/google/client.go b/prow/gangway/google/client.go new file mode 100644 index 0000000000000..b980bf6551f2c --- /dev/null +++ b/prow/gangway/google/client.go @@ -0,0 +1,160 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package google + +import ( + "context" + "errors" + "fmt" + "io/ioutil" + + "golang.org/x/oauth2" + googleOAuth "golang.org/x/oauth2/google" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" + + pb "k8s.io/test-infra/prow/gangway" +) + +// This is the client library for Go clients that need to access to the Prow +// API, aka Gangway, when Gangway is deployed in a GKE cluster and integrated +// with Cloud Endpoints. +// +// Go clients need to always append 2 things to the metadata of a gRPC call: +// +// 1. The JWT (authentication) token (to make the call identify itself as +// an allowlisted client in our api_config_auth.yaml configuration for Cloud +// Endpoints [1]), and +// +// 2. The API key (that is generated by the client's GCP Project). +// +// The JWT token is generated from a GCP Service Account key file (JSON). The +// API key is generated from the GCP user interface, like this: +// https://cloud.google.com/docs/authentication/api-keys#create. +// +// For us, the clients must supply the service account JSON key file, API key, +// audience, and finally the address where Gangway is being served. An example +// client application using this library is provided in the Prow codebase under +// prow/gangway/example/main.go. +// +// [1]: https://github.com/GoogleCloudPlatform/golang-samples/blob/e888c56cb843f475db4f79b391be999518e63db4/endpoints/getting-started-grpc/README.md#configuring-authentication-and-authenticating-requests + +type Client struct { + // JWT token-based authentication and GCP Project identification. + keyBytes []byte + audience string + tokenSource oauth2.TokenSource + + // apiKey identifies the GCP Project. + apiKey string + + addr string + conn *grpc.ClientConn + + // GRPC is the auto-generated gRPC client interface for gangway. + GRPC pb.ProwClient +} + +// NewFromFile creates a Gangway client from a JSON service account key file and an audience string. +func NewFromFile(keyFile, audience, apiKey, addr, clientPem string) (*Client, error) { + keyBytes, err := ioutil.ReadFile(keyFile) + if err != nil { + return nil, fmt.Errorf("Unable to read service account key file %s: %v", keyFile, err) + } + + return New(keyBytes, audience, apiKey, addr, clientPem) +} + +// New creates a new gRPC client. It does most of the work in NewFromFile(). +func New(keyBytes []byte, audience, apiKey, addr, clientPem string) (*Client, error) { + c := Client{} + + creds, err := credentials.NewClientTLSFromFile(clientPem, "") + if err != nil { + return nil, fmt.Errorf("could not process clientPem credentials: %v", err) + } + + c.addr = addr + + conn, err := grpc.Dial(c.addr, grpc.WithTransportCredentials(creds)) + if err != nil { + return nil, fmt.Errorf("could not connect to %q: %v", c.addr, err) + } + c.conn = conn + c.GRPC = pb.NewProwClient(c.conn) + + if len(audience) == 0 { + return nil, errors.New("audience cannot be empty") + } + + c.audience = audience + + if len(apiKey) == 0 { + return nil, errors.New("apiKey cannot be empty") + } + + c.apiKey = apiKey + + if len(keyBytes) == 0 { + return nil, errors.New("keyBytes cannot be empty") + } + + c.keyBytes = keyBytes + + tokenSource, err := googleOAuth.JWTAccessTokenSourceFromJSON(c.keyBytes, c.audience) + if err != nil { + return nil, fmt.Errorf("could not create tokenSource: %v", err) + } + + c.tokenSource = tokenSource + + return &c, nil +} + +// MkToken generates a new JWT token with a 1h TTL. This is apparently a +// cheap operation, according to +// https://github.com/GoogleCloudPlatform/golang-samples/blob/e7a5459d85661a35c5eb4f0b5759b7b30ac6ff90/endpoints/getting-started-grpc/client/main.go#L81-L88. +func (c *Client) MkToken() (string, error) { + jwt, err := c.tokenSource.Token() + if err != nil { + return "", fmt.Errorf("could not generate JSON Web Token: %v", err) + } + + return jwt.AccessToken, nil +} + +// EmbedCredentials is used to modify a provided context so that it has the the +// necessary token and apiKey attached to it in the metadata. +func (c *Client) EmbedCredentials(ctx context.Context) (context.Context, error) { + ctxWithCreds := metadata.AppendToOutgoingContext(ctx, "x-api-key", c.apiKey) + + token, err := c.MkToken() + if err != nil { + return ctxWithCreds, err + } + + fmt.Printf("using token %q\n", token) + + ctxWithCreds = metadata.AppendToOutgoingContext(ctxWithCreds, "Authorization", fmt.Sprintf("Bearer %s", token)) + + return ctxWithCreds, nil +} + +func (c *Client) Close() { + c.conn.Close() +}