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>
166 lines
5.0 KiB
Go
166 lines
5.0 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|