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>
79 lines
2.6 KiB
Go
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
|
|
}
|
|
}
|