github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/operations/operations_gcp.go (about)

     1  // Copyright 2016-2019, Pulumi Corporation.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package operations
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"reflect"
    22  	"strings"
    23  	"time"
    24  
    25  	gcplogging "cloud.google.com/go/logging/apiv2"
    26  	"google.golang.org/api/iterator"
    27  	"google.golang.org/api/option"
    28  	loggingpb "google.golang.org/genproto/googleapis/logging/v2"
    29  
    30  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
    31  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
    32  	"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
    33  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
    34  )
    35  
    36  // TODO[pulumi/pulumi#54] This should be factored out behind an OperationsProvider RPC interface and versioned with the
    37  // `pulumi-gcp` repo instead of statically linked into the engine.
    38  
    39  // GCPOperationsProvider creates an OperationsProvider capable of answering operational queries based on the
    40  // underlying resources of the `@pulumi/gcp` implementation.
    41  func GCPOperationsProvider(
    42  	config map[config.Key]string,
    43  	component *Resource) (Provider, error) {
    44  
    45  	ctx := context.TODO()
    46  	client, err := gcplogging.NewClient(ctx, option.WithScopes("https://www.googleapis.com/auth/logging.read"))
    47  	if err != nil {
    48  		return nil, err
    49  	}
    50  
    51  	prov := &gcpOpsProvider{
    52  		ctx:       ctx,
    53  		client:    client,
    54  		component: component,
    55  	}
    56  	return prov, nil
    57  }
    58  
    59  type gcpOpsProvider struct {
    60  	ctx       context.Context
    61  	client    *gcplogging.Client
    62  	component *Resource
    63  }
    64  
    65  var _ Provider = (*gcpOpsProvider)(nil)
    66  
    67  const (
    68  	// GCP resource types
    69  	gcpFunctionType = tokens.Type("gcp:cloudfunctions/function:Function")
    70  )
    71  
    72  func (ops *gcpOpsProvider) GetLogs(query LogQuery) (*[]LogEntry, error) {
    73  	state := ops.component.State
    74  	logging.V(6).Infof("GetLogs[%v]", state.URN)
    75  	switch state.Type {
    76  	case gcpFunctionType:
    77  		return ops.getFunctionLogs(state, query)
    78  	default:
    79  		// Else this resource kind does not produce any logs.
    80  		logging.V(6).Infof("GetLogs[%v] does not produce logs", state.URN)
    81  		return nil, nil
    82  	}
    83  }
    84  
    85  func (ops *gcpOpsProvider) getFunctionLogs(state *resource.State, query LogQuery) (*[]LogEntry, error) {
    86  	name := state.Outputs["name"].StringValue()
    87  	project := state.Outputs["project"].StringValue()
    88  	region := state.Outputs["region"].StringValue()
    89  
    90  	// These filters mirror what `gcloud functions logs read [function-name]` does to filter.
    91  	logFilter := []string{
    92  		`resource.type="cloud_function"`,
    93  		`resource.labels.region="` + region + `"`,
    94  		`logName:"cloud-functions"`,
    95  		`resource.labels.function_name="` + name + `"`,
    96  	}
    97  
    98  	if query.StartTime != nil {
    99  		logFilter = append(logFilter, fmt.Sprintf(`timestamp>="%s"`, query.StartTime.Format(time.RFC3339)))
   100  	}
   101  
   102  	if query.EndTime != nil {
   103  		logFilter = append(logFilter, fmt.Sprintf(`timestamp<="%s"`, query.EndTime.Format(time.RFC3339)))
   104  	}
   105  
   106  	req := &loggingpb.ListLogEntriesRequest{
   107  		ResourceNames: []string{"projects/" + project},
   108  		Filter:        strings.Join(logFilter, " "),
   109  	}
   110  
   111  	var logs []LogEntry
   112  
   113  	it := ops.client.ListLogEntries(ops.ctx, req)
   114  	for {
   115  		entry, err := it.Next()
   116  		if err == iterator.Done {
   117  			logging.V(5).Infof("GetLogs[%v] return %d logs", state.URN, len(logs))
   118  			return &logs, nil
   119  		}
   120  		if err != nil {
   121  			logging.V(5).Infof("GetLogs[%v] error iterating logs: %s", state.URN, err)
   122  			return nil, err
   123  		}
   124  
   125  		message, err := getLogEntryMessage(entry)
   126  		if err != nil {
   127  			logging.V(5).Infof("GetLogs[%v] error getting entry message, ignoring: %s", state.URN, err)
   128  			continue
   129  		}
   130  
   131  		logs = append(logs, LogEntry{
   132  			ID:        name,
   133  			Message:   message,
   134  			Timestamp: entry.GetTimestamp().Seconds * 1000,
   135  		})
   136  	}
   137  }
   138  
   139  // getLogEntryMessage gets the message for a log entry. There are many different underlying types for the message
   140  // payload. If we don't know how to decode a payload to a string, an error is returned.
   141  func getLogEntryMessage(e *loggingpb.LogEntry) (string, error) {
   142  	switch payload := e.GetPayload().(type) {
   143  	case *loggingpb.LogEntry_TextPayload:
   144  		return payload.TextPayload, nil
   145  
   146  	case *loggingpb.LogEntry_JsonPayload:
   147  		byts, err := json.Marshal(payload.JsonPayload)
   148  		if err != nil {
   149  			return "", fmt.Errorf("encoding to JSON: %w", err)
   150  		}
   151  		return string(byts), nil
   152  	default:
   153  		return "", fmt.Errorf("can't decode entry of type %s", reflect.TypeOf(e))
   154  	}
   155  }