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>
This commit is contained in:
2026-09-11 20:08:21 +00:00
co-authored by Claude Opus 5
parent 1c773c24ad
commit 1962365f44
9 changed files with 511 additions and 5 deletions
+165
View File
@@ -0,0 +1,165 @@
package gcp
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"net/url"
"sync"
"syscall"
"testing"
compute "cloud.google.com/go/compute/apiv1"
"cloud.google.com/go/compute/apiv1/computepb"
"google.golang.org/api/googleapi"
"google.golang.org/api/option"
"google.golang.org/protobuf/proto"
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
)
func TestCheck_reachable(t *testing.T) {
t.Parallel()
api := &fakeAPI{}
p := newTestProvider(api)
got := p.Check(context.Background())
if len(got) != 1 {
t.Fatalf("Check() returned %d targets, want 1", len(got))
}
ts := got[0]
if !ts.Ready || ts.Reason != provider.ReasonReachable || ts.Message != "" {
t.Errorf("Check() = %+v, want ready and Reachable", ts)
}
if ts.Name != "my-project" || ts.Address != "compute.googleapis.com" {
t.Errorf("target = %q at %q, want my-project at compute.googleapis.com", ts.Name, ts.Address)
}
want := &computepb.AggregatedListInstancesRequest{
Project: "my-project",
Filter: proto.String("labels.proxy-operator-managed = true"),
MaxResults: proto.Uint32(1),
ReturnPartialSuccess: proto.Bool(true),
}
if !proto.Equal(api.probeReq, want) {
t.Errorf("probe request = %v, want %v", api.probeReq, want)
}
}
func TestCheck_failureReasons(t *testing.T) {
t.Parallel()
ended, cancel := context.WithCancel(context.Background())
cancel()
tests := []struct {
name string
ctx context.Context
err error
want string
}{
{"unauthorized", context.Background(), gerr(401), provider.ReasonUnauthorized},
{"forbidden", context.Background(), gerr(403, "forbidden"), provider.ReasonForbidden},
{"api disabled", context.Background(), gerr(403, "accessNotConfigured"), provider.ReasonForbidden},
{"server error", context.Background(), gerr(500), provider.ReasonAPIError},
{"deadline", context.Background(), &url.Error{
Op: "Get", URL: "https://compute.googleapis.com/", Err: context.DeadlineExceeded,
}, provider.ReasonTimeout},
{"context ended", ended, context.Canceled, provider.ReasonTimeout},
{"connection refused", context.Background(), &url.Error{
Op: "Get", URL: "https://compute.googleapis.com/", Err: syscall.ECONNREFUSED,
}, provider.ReasonUnreachable},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p := newTestProvider(&fakeAPI{probeErr: tc.err})
got := p.Check(tc.ctx)
if len(got) != 1 {
t.Fatalf("Check() returned %d targets, want 1", len(got))
}
if got[0].Ready || got[0].Reason != tc.want || got[0].Message != tc.err.Error() {
t.Errorf("Check() = %+v, want not ready, reason %s, message %q", got[0], tc.want, tc.err.Error())
}
})
}
}
// TestRealInstances_Probe runs the real SDK client against a local server,
// since the point of Probe is how many requests the SDK makes.
func TestRealInstances_Probe(t *testing.T) {
t.Parallel()
tests := []struct {
name string
status int
body string
wantCode int
}{
{
name: "fetches only the first page",
status: http.StatusOK,
body: `{"items":{"zones/europe-west1-b":{"instances":[{"name":"proxy-a"}]}},"nextPageToken":"page-2"}`,
},
{
name: "empty project",
status: http.StatusOK,
body: `{}`,
},
{
name: "api error keeps its status",
status: http.StatusForbidden,
body: `{"error":{"code":403,"message":"Compute Engine API has not been used","errors":[{"reason":"accessNotConfigured"}]}}`,
wantCode: http.StatusForbidden,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var (
mu sync.Mutex
reqs []*url.URL
)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
reqs = append(reqs, r.URL)
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(tc.status)
_, _ = w.Write([]byte(tc.body))
}))
t.Cleanup(srv.Close)
ctx := context.Background()
c, err := compute.NewInstancesRESTClient(ctx, option.WithEndpoint(srv.URL), option.WithoutAuthentication())
if err != nil {
t.Fatalf("NewInstancesRESTClient: %v", err)
}
t.Cleanup(func() { _ = c.Close() })
err = (&realInstances{client: c}).Probe(ctx, &computepb.AggregatedListInstancesRequest{
Project: "my-project",
MaxResults: proto.Uint32(1),
})
var gerr *googleapi.Error
switch {
case tc.wantCode == 0 && err != nil:
t.Fatalf("Probe() = %v, want nil", err)
case tc.wantCode != 0 && (!errors.As(err, &gerr) || gerr.Code != tc.wantCode):
t.Fatalf("Probe() = %v, want a googleapi error with code %d", err, tc.wantCode)
}
mu.Lock()
defer mu.Unlock()
if len(reqs) != 1 {
t.Fatalf("server saw %d requests, want 1", len(reqs))
}
if got := reqs[0].Query().Get("maxResults"); got != "1" {
t.Errorf("maxResults = %q, want 1", got)
}
if want := "/compute/v1/projects/my-project/aggregated/instances"; reqs[0].Path != want {
t.Errorf("path = %q, want %q", reqs[0].Path, want)
}
})
}
}