Compare commits
2 Commits
c108a06a94
...
837e374228
| Author | SHA1 | Date | |
|---|---|---|---|
| 837e374228 | |||
| c137028364 |
@@ -0,0 +1,42 @@
|
|||||||
|
# Execution: Verbose (V-level) logging in the GCP provider
|
||||||
|
|
||||||
|
Plan: `docs/plans/2026-08-11-1742-gcp-provider-verbose-logging.md`
|
||||||
|
|
||||||
|
- [x] Step 0 — Save and commit the plan
|
||||||
|
- [x] Step 1 — V(1)/V(2) logging in `internal/provider/gcp` (gcp.go, errors.go)
|
||||||
|
- [x] Step 2 — Tests (verbosity tiers, error detail, cloud-init leak guard)
|
||||||
|
- [ ] Step 3 — CHANGELOG entry (after the user confirms it works live)
|
||||||
|
|
||||||
|
## Step 1 — logging in the provider
|
||||||
|
|
||||||
|
Went as planned: context-carried logger (`logf.FromContext(ctx).WithName("gcp")`),
|
||||||
|
V(1) one line per API call, V(2) request/list detail, opNames captured from the
|
||||||
|
`instancesAPI` seam instead of being discarded. `logAPIError` lives in
|
||||||
|
`errors.go` (next to `classify`, whose imports it shares) rather than `gcp.go`
|
||||||
|
as loosely implied by the plan — same package, so no behavioural difference.
|
||||||
|
These are the first `.V(n)` calls and the first logging import anywhere under
|
||||||
|
`internal/provider/`.
|
||||||
|
|
||||||
|
Worth noting: the gopls `errorsastype` suggestion fired on the new
|
||||||
|
`errors.As` in `logAPIError` (Go's newer `errors.AsType`); kept `errors.As`
|
||||||
|
for consistency with the three existing uses in the same file. Same for the
|
||||||
|
`newexpr` (`proto.String` → `new`) suggestions — the codebase consistently
|
||||||
|
uses `proto.String`.
|
||||||
|
|
||||||
|
## Step 2 — tests
|
||||||
|
|
||||||
|
`funcr.New` as the capturing sink, injected via `logr.NewContext`, exactly the
|
||||||
|
seam the plan predicted. One deviation: instead of a single
|
||||||
|
`TestLogging_verbosity` table, it split into three tests — `_verbosityTiers`
|
||||||
|
(table over V=0/1/2, incl. the cloud-init sentinel leak assertion),
|
||||||
|
`_apiErrorKeepsHTTPDetail` (403 quotaExceeded keeps `httpStatus`/reason at
|
||||||
|
V(1)), and `_treatedAsSuccessPathsAreExplicit` (409-on-create /
|
||||||
|
404-on-delete each log their "treated as success" line) — the last two
|
||||||
|
exercise fake error wiring that didn't fit the tier table cleanly.
|
||||||
|
|
||||||
|
Verified with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -race ./internal/provider/gcp/
|
||||||
|
go test ./...
|
||||||
|
```
|
||||||
87
docs/plans/2026-08-11-1742-gcp-provider-verbose-logging.md
Normal file
87
docs/plans/2026-08-11-1742-gcp-provider-verbose-logging.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Plan: Verbose (V-level) logging in the GCP provider
|
||||||
|
|
||||||
|
**Created:** 2026-08-11 17:42
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Debugging GCP provisioning is currently blind: the operator has zero `.V(n)` calls
|
||||||
|
anywhere, so `--zap-log-level=debug` (or any numeric level) reveals nothing about
|
||||||
|
what the GCP provider is doing — which API calls it makes, with what parameters,
|
||||||
|
and what came back. The goal: with debug/V-level logging enabled, see the details
|
||||||
|
of every GCP Compute API call (Insert/Get/Delete/AggregatedList) including a
|
||||||
|
summary of the response; with default `info` level, the provider stays as quiet
|
||||||
|
as today.
|
||||||
|
|
||||||
|
## Approach (the "how")
|
||||||
|
|
||||||
|
**Logger source — context-carried, not injected.** Provider methods all take
|
||||||
|
`ctx`, and the reconciler already builds a per-request logger
|
||||||
|
(`logf.FromContext(ctx)` in `internal/controller/proxy_controller.go:117`) that
|
||||||
|
carries the proxy's name/namespace. The GCP provider will do
|
||||||
|
`log := logf.FromContext(ctx).WithName("gcp").WithValues("provider", p.name)` at
|
||||||
|
the top of each public method. Zero wiring changes (no registry/constructor/struct
|
||||||
|
changes), and every provider log line automatically inherits the reconcile
|
||||||
|
context (which Proxy triggered it). Calls from the GC sweeper inherit its
|
||||||
|
`orphan-gc` logger name the same way.
|
||||||
|
|
||||||
|
**Verbosity scheme** (logr convention: `.Info()` = V(0), `debug` flag = V(1)):
|
||||||
|
|
||||||
|
- **V(1)** — one line per GCP API call, after it returns: operation, identifying
|
||||||
|
params, outcome. Examples:
|
||||||
|
- `Create`: `"GCP insert instance"` with `zone`, `name`, `machineType`,
|
||||||
|
`image`, `opName` (currently discarded at gcp.go:117 — capture it, it's the
|
||||||
|
only handle for correlating with GCP's operation log), plus a line for the
|
||||||
|
409-already-exists path.
|
||||||
|
- `Get`: `"GCP get instance"` with `zone`, `name`, `status`, mapped `state`, `ip`.
|
||||||
|
- `Delete`: `"GCP delete instance"` with `zone`, `name`, `opName`, and the
|
||||||
|
404-treated-as-success path.
|
||||||
|
- `ListByTag`: `"GCP aggregated list"` with `filter`, `count`.
|
||||||
|
- Error paths at V(1) too: log the raw classification (HTTP status / reason
|
||||||
|
from `googleapi.Error`) before it's wrapped, since the wrapped error the
|
||||||
|
reconciler sees is coarser.
|
||||||
|
- **V(2)** — request/response detail: full curated insert-request summary
|
||||||
|
(network, networkTag, diskSizeGB, port, labels, `cloudInitBytes` = `len`),
|
||||||
|
per-instance lines in `ListByTag` (id, state, uid, age).
|
||||||
|
|
||||||
|
**Curated fields, never raw proto dumps.** `CreateRequest.CloudInit` is resolved
|
||||||
|
user-data possibly sourced from a Secret, and it lands in the insert request's
|
||||||
|
metadata — so logging the request proto wholesale would leak it. Log named safe
|
||||||
|
fields only; for cloud-init, log only its byte length. This is a hard rule, and
|
||||||
|
a test asserts it.
|
||||||
|
|
||||||
|
**Where the calls live: the `Provider` methods in
|
||||||
|
`internal/provider/gcp/gcp.go`** (Create/Get/Delete/ListByTag), not in
|
||||||
|
`realInstances` (deliberately untested by design, gcp.go:86) and not an HTTP
|
||||||
|
round-tripper (would log auth headers/user-data, unredactable). The
|
||||||
|
`instancesAPI` fake seam (`newWithAPI`, gcp.go:96) keeps everything testable.
|
||||||
|
To surface `opName`, change `Provider.Create`/`Delete` to capture the string
|
||||||
|
their `instancesAPI` calls already return instead of discarding it.
|
||||||
|
|
||||||
|
## Files to change
|
||||||
|
|
||||||
|
- `internal/provider/gcp/gcp.go` — add `logf` import; V(1)/V(2) logging in
|
||||||
|
`Create`, `Get`, `Delete`, `ListByTag`; capture opNames. Only file with
|
||||||
|
production changes.
|
||||||
|
- `internal/provider/gcp/gcp_test.go` — new table-driven test
|
||||||
|
`TestLogging_verbosity` (name TBD per house `Test<Function>_<scenario>`
|
||||||
|
style): inject a capturing logger via `logf.IntoContext(ctx, funcr.New(...))`
|
||||||
|
(`github.com/go-logr/logr/funcr`, logr already a direct dep), assert:
|
||||||
|
- at V(1): expected message + keys per operation (incl. opName),
|
||||||
|
- at V(0): nothing logged,
|
||||||
|
- **cloud-init content never appears in any log output** (grep the captured
|
||||||
|
lines for a sentinel string placed in `CloudInit`).
|
||||||
|
- No changes to `provider.Provider` interface, registry, `cmd/main.go`,
|
||||||
|
manifests, or the kubernetes provider (it can copy this pattern later).
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test -race ./internal/provider/gcp/...
|
||||||
|
go build ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional live check: run the manager with `--zap-log-level=2` against the GCP
|
||||||
|
project and confirm insert/get lines appear during a Proxy reconcile, and that
|
||||||
|
`--zap-log-level=info` stays quiet.
|
||||||
|
|
||||||
|
Also append a CHANGELOG.md entry per house convention once confirmed working.
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
|
"github.com/go-logr/logr"
|
||||||
"google.golang.org/api/googleapi"
|
"google.golang.org/api/googleapi"
|
||||||
|
|
||||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
||||||
@@ -41,6 +42,25 @@ func (p *Provider) wrapErr(op, id string, err error) error {
|
|||||||
return provider.Wrap(classify(err), op, p.name, id, err)
|
return provider.Wrap(classify(err), op, p.name, id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logAPIError records the raw googleapi error shape (HTTP status, reasons)
|
||||||
|
// at V(1) — classify collapses it onto the coarser provider taxonomy, so
|
||||||
|
// this line is the only place the original status survives.
|
||||||
|
func logAPIError(log logr.Logger, op string, err error) {
|
||||||
|
if !log.V(1).Enabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
kv := []any{"op", op, "error", err.Error()}
|
||||||
|
var gerr *googleapi.Error
|
||||||
|
if errors.As(err, &gerr) {
|
||||||
|
reasons := make([]string, 0, len(gerr.Errors))
|
||||||
|
for _, item := range gerr.Errors {
|
||||||
|
reasons = append(reasons, item.Reason)
|
||||||
|
}
|
||||||
|
kv = append(kv, "httpStatus", gerr.Code, "reasons", reasons)
|
||||||
|
}
|
||||||
|
log.V(1).Info("GCP API call failed", kv...)
|
||||||
|
}
|
||||||
|
|
||||||
func hasReason(gerr *googleapi.Error, reasons ...string) bool {
|
func hasReason(gerr *googleapi.Error, reasons ...string) bool {
|
||||||
for _, item := range gerr.Errors {
|
for _, item := range gerr.Errors {
|
||||||
if slices.Contains(reasons, item.Reason) {
|
if slices.Contains(reasons, item.Reason) {
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ import (
|
|||||||
|
|
||||||
compute "cloud.google.com/go/compute/apiv1"
|
compute "cloud.google.com/go/compute/apiv1"
|
||||||
"cloud.google.com/go/compute/apiv1/computepb"
|
"cloud.google.com/go/compute/apiv1/computepb"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
"google.golang.org/api/iterator"
|
"google.golang.org/api/iterator"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
||||||
)
|
)
|
||||||
@@ -101,6 +103,12 @@ func newWithAPI(pc provider.ProviderConfig, api instancesAPI) *Provider {
|
|||||||
return &Provider{name: pc.Name, cfg: withDefaults(cfg), api: api}
|
return &Provider{name: pc.Name, cfg: withDefaults(cfg), api: api}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logger derives the request-scoped logger from ctx, so provider lines
|
||||||
|
// inherit the reconcile context (which Proxy triggered the call).
|
||||||
|
func (p *Provider) logger(ctx context.Context) logr.Logger {
|
||||||
|
return logf.FromContext(ctx).WithName("gcp").WithValues("provider", p.name)
|
||||||
|
}
|
||||||
|
|
||||||
// Create submits the insert and returns immediately with the
|
// Create submits the insert and returns immediately with the
|
||||||
// zone-qualified providerID. A 409 alreadyExists is success — the
|
// zone-qualified providerID. A 409 alreadyExists is success — the
|
||||||
// deterministic instance name means a repeat call after a crash found the
|
// deterministic instance name means a repeat call after a crash found the
|
||||||
@@ -113,8 +121,28 @@ func (p *Provider) Create(ctx context.Context, req provider.CreateRequest) (stri
|
|||||||
"gcp requires placement.zone, placement.machineType and placement.image (got zone=%q machineType=%q image=%q)",
|
"gcp requires placement.zone, placement.machineType and placement.image (got zone=%q machineType=%q image=%q)",
|
||||||
pl.Zone, pl.MachineType, pl.Image))
|
pl.Zone, pl.MachineType, pl.Image))
|
||||||
}
|
}
|
||||||
|
log := p.logger(ctx)
|
||||||
id := formatProviderID(pl.Zone, req.Name)
|
id := formatProviderID(pl.Zone, req.Name)
|
||||||
if _, err := p.api.Insert(ctx, buildInsertRequest(p.cfg, req)); err != nil && !isAlreadyExists(err) {
|
insertReq := buildInsertRequest(p.cfg, req)
|
||||||
|
// Curated fields only: the request proto embeds the cloud-init
|
||||||
|
// user-data, which may be Secret-sourced and must never reach logs.
|
||||||
|
log.V(2).Info("GCP insert request built",
|
||||||
|
"zone", pl.Zone, "name", req.Name,
|
||||||
|
"network", p.cfg.Network, "networkTag", p.cfg.NetworkTag,
|
||||||
|
"diskSizeGb", p.cfg.DiskSizeGB, "port", req.Port,
|
||||||
|
"labels", insertReq.GetInstanceResource().GetLabels(),
|
||||||
|
"cloudInitBytes", len(req.CloudInit))
|
||||||
|
opName, err := p.api.Insert(ctx, insertReq)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
log.V(1).Info("GCP instance insert submitted",
|
||||||
|
"zone", pl.Zone, "name", req.Name,
|
||||||
|
"machineType", pl.MachineType, "image", pl.Image, "opName", opName)
|
||||||
|
case isAlreadyExists(err):
|
||||||
|
log.V(1).Info("GCP instance already exists, insert treated as success",
|
||||||
|
"zone", pl.Zone, "name", req.Name)
|
||||||
|
default:
|
||||||
|
logAPIError(log, "create", err)
|
||||||
return "", p.wrapErr("create", id, err)
|
return "", p.wrapErr("create", id, err)
|
||||||
}
|
}
|
||||||
return id, nil
|
return id, nil
|
||||||
@@ -128,15 +156,21 @@ func (p *Provider) Get(ctx context.Context, providerID string) (*provider.Instan
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, provider.Wrap(provider.ErrPermanent, "get", p.name, providerID, err)
|
return nil, provider.Wrap(provider.ErrPermanent, "get", p.name, providerID, err)
|
||||||
}
|
}
|
||||||
|
log := p.logger(ctx)
|
||||||
inst, err := p.api.Get(ctx, &computepb.GetInstanceRequest{
|
inst, err := p.api.Get(ctx, &computepb.GetInstanceRequest{
|
||||||
Project: p.cfg.Project,
|
Project: p.cfg.Project,
|
||||||
Zone: zone,
|
Zone: zone,
|
||||||
Instance: name,
|
Instance: name,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logAPIError(log, "get", err)
|
||||||
return nil, p.wrapErr("get", providerID, err)
|
return nil, p.wrapErr("get", providerID, err)
|
||||||
}
|
}
|
||||||
return toInstance(inst, zone), nil
|
out := toInstance(inst, zone)
|
||||||
|
log.V(1).Info("GCP instance fetched",
|
||||||
|
"zone", zone, "name", name,
|
||||||
|
"status", inst.GetStatus(), "state", out.State, "ip", out.IP)
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete submits the delete and returns; deleting an instance that is
|
// Delete submits the delete and returns; deleting an instance that is
|
||||||
@@ -146,11 +180,21 @@ func (p *Provider) Delete(ctx context.Context, providerID string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return provider.Wrap(provider.ErrPermanent, "delete", p.name, providerID, err)
|
return provider.Wrap(provider.ErrPermanent, "delete", p.name, providerID, err)
|
||||||
}
|
}
|
||||||
if _, err := p.api.Delete(ctx, &computepb.DeleteInstanceRequest{
|
log := p.logger(ctx)
|
||||||
|
opName, err := p.api.Delete(ctx, &computepb.DeleteInstanceRequest{
|
||||||
Project: p.cfg.Project,
|
Project: p.cfg.Project,
|
||||||
Zone: zone,
|
Zone: zone,
|
||||||
Instance: name,
|
Instance: name,
|
||||||
}); err != nil && !isNotFound(err) {
|
})
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
log.V(1).Info("GCP instance delete submitted",
|
||||||
|
"zone", zone, "name", name, "opName", opName)
|
||||||
|
case isNotFound(err):
|
||||||
|
log.V(1).Info("GCP instance already gone, delete treated as success",
|
||||||
|
"zone", zone, "name", name)
|
||||||
|
default:
|
||||||
|
logAPIError(log, "delete", err)
|
||||||
return p.wrapErr("delete", providerID, err)
|
return p.wrapErr("delete", providerID, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -160,17 +204,24 @@ func (p *Provider) Delete(ctx context.Context, providerID string) error {
|
|||||||
// ReturnPartialSuccess matters: without it one unreachable zone fails the
|
// ReturnPartialSuccess matters: without it one unreachable zone fails the
|
||||||
// entire GC sweep.
|
// entire GC sweep.
|
||||||
func (p *Provider) ListByTag(ctx context.Context) ([]provider.Instance, error) {
|
func (p *Provider) ListByTag(ctx context.Context) ([]provider.Instance, error) {
|
||||||
|
log := p.logger(ctx)
|
||||||
|
filter := fmt.Sprintf("labels.%s = %s", provider.LabelManaged, provider.LabelManagedYes)
|
||||||
instances, err := p.api.AggregatedList(ctx, &computepb.AggregatedListInstancesRequest{
|
instances, err := p.api.AggregatedList(ctx, &computepb.AggregatedListInstancesRequest{
|
||||||
Project: p.cfg.Project,
|
Project: p.cfg.Project,
|
||||||
Filter: proto.String(fmt.Sprintf("labels.%s = %s", provider.LabelManaged, provider.LabelManagedYes)),
|
Filter: proto.String(filter),
|
||||||
ReturnPartialSuccess: proto.Bool(true),
|
ReturnPartialSuccess: proto.Bool(true),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logAPIError(log, "list", err)
|
||||||
return nil, p.wrapErr("list", "", err)
|
return nil, p.wrapErr("list", "", err)
|
||||||
}
|
}
|
||||||
|
log.V(1).Info("GCP instances listed", "filter", filter, "count", len(instances))
|
||||||
out := make([]provider.Instance, 0, len(instances))
|
out := make([]provider.Instance, 0, len(instances))
|
||||||
for _, inst := range instances {
|
for _, inst := range instances {
|
||||||
out = append(out, *toInstance(inst, lastPathSegment(inst.GetZone())))
|
conv := toInstance(inst, lastPathSegment(inst.GetZone()))
|
||||||
|
out = append(out, *conv)
|
||||||
|
log.V(2).Info("GCP listed instance",
|
||||||
|
"id", conv.ID, "state", conv.State, "uid", conv.UID, "createdAt", conv.CreatedAt)
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ package gcp
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"cloud.google.com/go/compute/apiv1/computepb"
|
"cloud.google.com/go/compute/apiv1/computepb"
|
||||||
|
"github.com/go-logr/logr"
|
||||||
|
"github.com/go-logr/logr/funcr"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
||||||
@@ -269,3 +272,172 @@ func TestParseProviderID_roundTrip(t *testing.T) {
|
|||||||
t.Errorf("round trip = %s/%s (%v), want europe-west1-b/proxy-abc", zone, name, err)
|
t.Errorf("round trip = %s/%s (%v), want europe-west1-b/proxy-abc", zone, name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// captureContext returns a ctx carrying a funcr logger that records every
|
||||||
|
// emitted line, capped at the given verbosity — the test stand-in for
|
||||||
|
// --zap-log-level=<verbosity>.
|
||||||
|
func captureContext(verbosity int) (context.Context, *[]string) {
|
||||||
|
lines := &[]string{}
|
||||||
|
log := funcr.New(func(prefix, args string) {
|
||||||
|
*lines = append(*lines, prefix+" "+args)
|
||||||
|
}, funcr.Options{Verbosity: verbosity})
|
||||||
|
return logr.NewContext(context.Background(), log), lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func runningInstance() *computepb.Instance {
|
||||||
|
return &computepb.Instance{
|
||||||
|
Name: proto.String("proxy-abc123def456ghij"),
|
||||||
|
Status: proto.String("RUNNING"),
|
||||||
|
Zone: proto.String("https://www.googleapis.com/compute/v1/projects/my-project/zones/europe-west1-b"),
|
||||||
|
CreationTimestamp: proto.String("2026-08-09T10:00:00+02:00"),
|
||||||
|
Labels: map[string]string{
|
||||||
|
provider.LabelManaged: provider.LabelManagedYes,
|
||||||
|
provider.LabelUID: "uid-1",
|
||||||
|
},
|
||||||
|
NetworkInterfaces: []*computepb.NetworkInterface{{
|
||||||
|
AccessConfigs: []*computepb.AccessConfig{{NatIP: proto.String("34.1.2.3")}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAllOps(t *testing.T, ctx context.Context, req provider.CreateRequest) {
|
||||||
|
t.Helper()
|
||||||
|
inst := runningInstance()
|
||||||
|
p := newTestProvider(&fakeAPI{getInst: inst, listInsts: []*computepb.Instance{inst}})
|
||||||
|
if _, err := p.Create(ctx, req); err != nil {
|
||||||
|
t.Fatalf("Create: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := p.Get(ctx, "zones/europe-west1-b/instances/proxy-abc123def456ghij"); err != nil {
|
||||||
|
t.Fatalf("Get: %v", err)
|
||||||
|
}
|
||||||
|
if err := p.Delete(ctx, "zones/europe-west1-b/instances/proxy-abc123def456ghij"); err != nil {
|
||||||
|
t.Fatalf("Delete: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := p.ListByTag(ctx); err != nil {
|
||||||
|
t.Fatalf("ListByTag: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogging_verbosityTiers(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
verbosity int
|
||||||
|
wantLines []string
|
||||||
|
absentLines []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "v0 stays silent",
|
||||||
|
verbosity: 0,
|
||||||
|
absentLines: []string{
|
||||||
|
"GCP instance insert submitted",
|
||||||
|
"GCP instance fetched",
|
||||||
|
"GCP instance delete submitted",
|
||||||
|
"GCP instances listed",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "v1 logs one line per API call",
|
||||||
|
verbosity: 1,
|
||||||
|
wantLines: []string{
|
||||||
|
`"msg"="GCP instance insert submitted"`,
|
||||||
|
`"opName"="op-insert"`,
|
||||||
|
`"msg"="GCP instance fetched"`,
|
||||||
|
`"status"="RUNNING"`,
|
||||||
|
`"msg"="GCP instance delete submitted"`,
|
||||||
|
`"opName"="op-delete"`,
|
||||||
|
`"msg"="GCP instances listed"`,
|
||||||
|
`"provider"="gcp-eu"`,
|
||||||
|
},
|
||||||
|
absentLines: []string{
|
||||||
|
"GCP insert request built",
|
||||||
|
"GCP listed instance",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "v2 adds request and per-instance detail",
|
||||||
|
verbosity: 2,
|
||||||
|
wantLines: []string{
|
||||||
|
`"msg"="GCP insert request built"`,
|
||||||
|
`"cloudInitBytes"=`,
|
||||||
|
`"msg"="GCP listed instance"`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, lines := captureContext(tc.verbosity)
|
||||||
|
req := testCreateRequest()
|
||||||
|
const sentinel = "SENTINEL-cloud-init-must-never-be-logged"
|
||||||
|
req.CloudInit = sentinel
|
||||||
|
|
||||||
|
runAllOps(t, ctx, req)
|
||||||
|
|
||||||
|
joined := strings.Join(*lines, "\n")
|
||||||
|
if tc.verbosity == 0 && len(*lines) != 0 {
|
||||||
|
t.Errorf("verbosity 0 logged %d lines:\n%s", len(*lines), joined)
|
||||||
|
}
|
||||||
|
for _, want := range tc.wantLines {
|
||||||
|
if !strings.Contains(joined, want) {
|
||||||
|
t.Errorf("output missing %q:\n%s", want, joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, absent := range tc.absentLines {
|
||||||
|
if strings.Contains(joined, absent) {
|
||||||
|
t.Errorf("output unexpectedly contains %q:\n%s", absent, joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(joined, sentinel) {
|
||||||
|
t.Errorf("cloud-init content leaked into logs:\n%s", joined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogging_apiErrorKeepsHTTPDetail(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, lines := captureContext(1)
|
||||||
|
p := newTestProvider(&fakeAPI{insertErr: gerr(403, "quotaExceeded")})
|
||||||
|
|
||||||
|
if _, err := p.Create(ctx, testCreateRequest()); err == nil {
|
||||||
|
t.Fatal("Create: want error")
|
||||||
|
}
|
||||||
|
|
||||||
|
joined := strings.Join(*lines, "\n")
|
||||||
|
for _, want := range []string{
|
||||||
|
`"msg"="GCP API call failed"`,
|
||||||
|
`"httpStatus"=403`,
|
||||||
|
`"quotaExceeded"`,
|
||||||
|
`"op"="create"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(joined, want) {
|
||||||
|
t.Errorf("output missing %q:\n%s", want, joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogging_treatedAsSuccessPathsAreExplicit(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
ctx, lines := captureContext(1)
|
||||||
|
p := newTestProvider(&fakeAPI{insertErr: gerr(409), deleteErr: gerr(404)})
|
||||||
|
|
||||||
|
if _, err := p.Create(ctx, testCreateRequest()); err != nil {
|
||||||
|
t.Fatalf("Create with 409: %v", err)
|
||||||
|
}
|
||||||
|
if err := p.Delete(ctx, "zones/z/instances/gone"); err != nil {
|
||||||
|
t.Fatalf("Delete with 404: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
joined := strings.Join(*lines, "\n")
|
||||||
|
for _, want := range []string{
|
||||||
|
`"msg"="GCP instance already exists, insert treated as success"`,
|
||||||
|
`"msg"="GCP instance already gone, delete treated as success"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(joined, want) {
|
||||||
|
t.Errorf("output missing %q:\n%s", want, joined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user