Files
kacerrandClaude Opus 5 1962365f44 Add the provider Checker contract and the kubernetes and GCP checks
Checker is an optional interface next to Provider: Check returns one
verdict per target and never fails as a whole. The kubernetes provider
lists at most one managed Pod cluster-wide (the verb and scope GC relies
on); the GCP provider fetches a single page of at most one managed
instance through a new Probe seam method, since the SDK treats
MaxResults as a page size and the existing AggregatedList drains every
page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 20:08:21 +00:00

79 lines
2.6 KiB
Go

package gcp
import (
"context"
"errors"
"net/http"
"time"
"cloud.google.com/go/compute/apiv1/computepb"
"google.golang.org/api/googleapi"
"google.golang.org/api/iterator"
"google.golang.org/protobuf/proto"
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
)
var _ provider.Checker = (*Provider)(nil)
// computeAddress is the endpoint the SDK talks to, shown only to humans.
const computeAddress = "compute.googleapis.com"
// managedFilter selects the instances this operator created.
const managedFilter = "labels." + provider.LabelManaged + " = " + provider.LabelManagedYes
// Check asks the project for one page holding at most one managed
// instance. That exercises the credentials, the Compute API and the
// compute.instances.list permission orphan GC depends on, in a single
// request whatever the project's size. The target is the project: the
// provider config names no zones.
func (p *Provider) Check(ctx context.Context) []provider.TargetStatus {
start := time.Now()
err := p.api.Probe(ctx, &computepb.AggregatedListInstancesRequest{
Project: p.cfg.Project,
Filter: proto.String(managedFilter),
MaxResults: proto.Uint32(1),
ReturnPartialSuccess: proto.Bool(true),
})
ts := provider.TargetStatus{Name: p.cfg.Project, Address: computeAddress, Latency: time.Since(start)}
if err != nil {
logAPIError(p.logger(ctx), "check", err)
ts.Reason = checkReason(ctx, err)
ts.Message = err.Error()
return []provider.TargetStatus{ts}
}
ts.Ready = true
ts.Reason = provider.ReasonReachable
return []provider.TargetStatus{ts}
}
// Probe fetches the first page of an aggregated list and discards it. The
// SDK treats MaxResults as the page size, so AggregatedList, which drains
// the iterator, would still walk every page of the project.
func (r *realInstances) Probe(ctx context.Context, req *computepb.AggregatedListInstancesRequest) error {
_, err := r.client.AggregatedList(ctx, req).Next()
if err == iterator.Done {
return nil
}
return err
}
// checkReason maps a failed probe to a reason. A token that cannot be
// obtained never reaches the API, so it has no status code and lands on
// Unreachable, with the detail in the message.
func checkReason(ctx context.Context, err error) string {
var gerr *googleapi.Error
switch {
case ctx.Err() != nil, errors.Is(err, context.DeadlineExceeded):
return provider.ReasonTimeout
case !errors.As(err, &gerr):
return provider.ReasonUnreachable
case gerr.Code == http.StatusUnauthorized:
return provider.ReasonUnauthorized
case gerr.Code == http.StatusForbidden:
return provider.ReasonForbidden
default:
return provider.ReasonAPIError
}
}