pulumi/pkg/operations/operations_gcp.go

157 lines
4.7 KiB
Go

// Copyright 2016-2019, Pulumi Corporation.
//
// 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 operations
import (
"context"
"encoding/json"
"fmt"
"reflect"
"strings"
"time"
gcplogging "cloud.google.com/go/logging/apiv2"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
loggingpb "google.golang.org/genproto/googleapis/logging/v2"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
)
// TODO[pulumi/pulumi#54] This should be factored out behind an OperationsProvider RPC interface and versioned with the
// `pulumi-gcp` repo instead of statically linked into the engine.
// GCPOperationsProvider creates an OperationsProvider capable of answering operational queries based on the
// underlying resources of the `@pulumi/gcp` implementation.
func GCPOperationsProvider(
config map[config.Key]string,
component *Resource,
) (Provider, error) {
ctx := context.TODO()
client, err := gcplogging.NewClient(ctx, option.WithScopes("https://www.googleapis.com/auth/logging.read"))
if err != nil {
return nil, err
}
prov := &gcpOpsProvider{
ctx: ctx,
client: client,
component: component,
}
return prov, nil
}
type gcpOpsProvider struct {
ctx context.Context
client *gcplogging.Client
component *Resource
}
var _ Provider = (*gcpOpsProvider)(nil)
const (
// GCP resource types
gcpFunctionType = tokens.Type("gcp:cloudfunctions/function:Function")
)
func (ops *gcpOpsProvider) GetLogs(query LogQuery) (*[]LogEntry, error) {
state := ops.component.State
logging.V(6).Infof("GetLogs[%v]", state.URN)
//exhaustive:ignore
switch state.Type {
case gcpFunctionType:
return ops.getFunctionLogs(state, query)
default:
// Else this resource kind does not produce any logs.
logging.V(6).Infof("GetLogs[%v] does not produce logs", state.URN)
return nil, nil
}
}
func (ops *gcpOpsProvider) getFunctionLogs(state *resource.State, query LogQuery) (*[]LogEntry, error) {
name := state.Outputs["name"].StringValue()
project := state.Outputs["project"].StringValue()
region := state.Outputs["region"].StringValue()
// These filters mirror what `gcloud functions logs read [function-name]` does to filter.
logFilter := []string{
`resource.type="cloud_function"`,
`resource.labels.region="` + region + `"`,
`logName:"cloud-functions"`,
`resource.labels.function_name="` + name + `"`,
}
if query.StartTime != nil {
logFilter = append(logFilter, fmt.Sprintf(`timestamp>="%s"`, query.StartTime.Format(time.RFC3339)))
}
if query.EndTime != nil {
logFilter = append(logFilter, fmt.Sprintf(`timestamp<="%s"`, query.EndTime.Format(time.RFC3339)))
}
req := &loggingpb.ListLogEntriesRequest{
ResourceNames: []string{"projects/" + project},
Filter: strings.Join(logFilter, " "),
}
var logs []LogEntry
it := ops.client.ListLogEntries(ops.ctx, req)
for {
entry, err := it.Next()
if err == iterator.Done {
logging.V(5).Infof("GetLogs[%v] return %d logs", state.URN, len(logs))
return &logs, nil
}
if err != nil {
logging.V(5).Infof("GetLogs[%v] error iterating logs: %s", state.URN, err)
return nil, err
}
message, err := getLogEntryMessage(entry)
if err != nil {
logging.V(5).Infof("GetLogs[%v] error getting entry message, ignoring: %s", state.URN, err)
continue
}
logs = append(logs, LogEntry{
ID: name,
Message: message,
Timestamp: entry.GetTimestamp().Seconds * 1000,
})
}
}
// getLogEntryMessage gets the message for a log entry. There are many different underlying types for the message
// payload. If we don't know how to decode a payload to a string, an error is returned.
func getLogEntryMessage(e *loggingpb.LogEntry) (string, error) {
switch payload := e.GetPayload().(type) {
case *loggingpb.LogEntry_TextPayload:
return payload.TextPayload, nil
case *loggingpb.LogEntry_JsonPayload:
byts, err := json.Marshal(payload.JsonPayload)
if err != nil {
return "", fmt.Errorf("encoding to JSON: %w", err)
}
return string(byts), nil
default:
return "", fmt.Errorf("can't decode entry of type %s", reflect.TypeOf(e))
}
}