272 lines
8.7 KiB
Go
272 lines
8.7 KiB
Go
package gcp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"cloud.google.com/go/compute/apiv1/computepb"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
|
)
|
|
|
|
// fakeAPI implements the instancesAPI seam.
|
|
type fakeAPI struct {
|
|
insertReq *computepb.InsertInstanceRequest
|
|
insertErr error
|
|
|
|
getReq *computepb.GetInstanceRequest
|
|
getInst *computepb.Instance
|
|
getErr error
|
|
|
|
deleteReq *computepb.DeleteInstanceRequest
|
|
deleteErr error
|
|
|
|
listReq *computepb.AggregatedListInstancesRequest
|
|
listInsts []*computepb.Instance
|
|
listErr error
|
|
}
|
|
|
|
func (f *fakeAPI) Insert(_ context.Context, req *computepb.InsertInstanceRequest) (string, error) {
|
|
f.insertReq = req
|
|
return "op-insert", f.insertErr
|
|
}
|
|
|
|
func (f *fakeAPI) Get(_ context.Context, req *computepb.GetInstanceRequest) (*computepb.Instance, error) {
|
|
f.getReq = req
|
|
return f.getInst, f.getErr
|
|
}
|
|
|
|
func (f *fakeAPI) Delete(_ context.Context, req *computepb.DeleteInstanceRequest) (string, error) {
|
|
f.deleteReq = req
|
|
return "op-delete", f.deleteErr
|
|
}
|
|
|
|
func (f *fakeAPI) AggregatedList(_ context.Context, req *computepb.AggregatedListInstancesRequest) ([]*computepb.Instance, error) {
|
|
f.listReq = req
|
|
return f.listInsts, f.listErr
|
|
}
|
|
|
|
func newTestProvider(api *fakeAPI) *Provider {
|
|
return newWithAPI(provider.ProviderConfig{
|
|
Name: "gcp-eu",
|
|
Type: "gcp",
|
|
GCP: &provider.GCPConfig{Project: "my-project"},
|
|
}, api)
|
|
}
|
|
|
|
func TestCreate_returnsZoneQualifiedID(t *testing.T) {
|
|
t.Parallel()
|
|
api := &fakeAPI{}
|
|
p := newTestProvider(api)
|
|
|
|
id, err := p.Create(context.Background(), testCreateRequest())
|
|
if err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
if want := "zones/europe-west1-b/instances/proxy-abc123def456ghij"; id != want {
|
|
t.Errorf("providerID = %s, want %s", id, want)
|
|
}
|
|
if api.insertReq.Project != "my-project" || api.insertReq.Zone != "europe-west1-b" {
|
|
t.Errorf("insert sent to %s/%s, want my-project/europe-west1-b", api.insertReq.Project, api.insertReq.Zone)
|
|
}
|
|
}
|
|
|
|
func TestCreate_alreadyExistsIsSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
api := &fakeAPI{insertErr: gerr(409)}
|
|
p := newTestProvider(api)
|
|
|
|
id, err := p.Create(context.Background(), testCreateRequest())
|
|
if err != nil {
|
|
t.Fatalf("Create after crash (409): %v — alreadyExists must be success", err)
|
|
}
|
|
if want := "zones/europe-west1-b/instances/proxy-abc123def456ghij"; id != want {
|
|
t.Errorf("providerID = %s, want %s", id, want)
|
|
}
|
|
}
|
|
|
|
func TestCreate_incompletePlacementIsPermanent(t *testing.T) {
|
|
t.Parallel()
|
|
api := &fakeAPI{}
|
|
p := newTestProvider(api)
|
|
req := testCreateRequest()
|
|
req.Placement.MachineType = ""
|
|
|
|
_, err := p.Create(context.Background(), req)
|
|
if provider.Class(err) != provider.ErrPermanent {
|
|
t.Errorf("Class = %v, want ErrPermanent for missing placement", provider.Class(err))
|
|
}
|
|
if api.insertReq != nil {
|
|
t.Error("Insert was called despite invalid placement")
|
|
}
|
|
}
|
|
|
|
func TestCreate_quotaErrorClassified(t *testing.T) {
|
|
t.Parallel()
|
|
p := newTestProvider(&fakeAPI{insertErr: gerr(403, "quotaExceeded")})
|
|
_, err := p.Create(context.Background(), testCreateRequest())
|
|
if provider.Class(err) != provider.ErrQuotaExceeded {
|
|
t.Errorf("Class = %v, want ErrQuotaExceeded", provider.Class(err))
|
|
}
|
|
}
|
|
|
|
func TestGet_stateMapping(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
status string
|
|
natIP string
|
|
wantState provider.InstanceState
|
|
wantIP string
|
|
}{
|
|
{name: "provisioning", status: "PROVISIONING", wantState: provider.StateProvisioning},
|
|
{name: "staging", status: "STAGING", wantState: provider.StateProvisioning},
|
|
{name: "repairing", status: "REPAIRING", wantState: provider.StateProvisioning},
|
|
{name: "running without NatIP stays provisioning", status: "RUNNING", wantState: provider.StateProvisioning},
|
|
{name: "running with NatIP", status: "RUNNING", natIP: "34.1.2.3", wantState: provider.StateRunning, wantIP: "34.1.2.3"},
|
|
{name: "stopped", status: "STOPPED", wantState: provider.StateStopped},
|
|
{name: "suspended", status: "SUSPENDED", wantState: provider.StateStopped},
|
|
{name: "terminated", status: "TERMINATED", wantState: provider.StateTerminated},
|
|
{name: "unknown status maps to stopped for recreation", status: "SOMETHING_NEW", wantState: provider.StateStopped},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
inst := &computepb.Instance{
|
|
Name: proto.String("proxy-abc"),
|
|
Status: proto.String(tc.status),
|
|
CreationTimestamp: proto.String("2026-08-09T10:00:00+02:00"),
|
|
Labels: map[string]string{
|
|
provider.LabelUID: "uid-1",
|
|
},
|
|
}
|
|
if tc.natIP != "" {
|
|
inst.NetworkInterfaces = []*computepb.NetworkInterface{{
|
|
AccessConfigs: []*computepb.AccessConfig{{NatIP: proto.String(tc.natIP)}},
|
|
}}
|
|
}
|
|
p := newTestProvider(&fakeAPI{getInst: inst})
|
|
|
|
got, err := p.Get(context.Background(), "zones/europe-west1-b/instances/proxy-abc")
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if got.State != tc.wantState || got.IP != tc.wantIP {
|
|
t.Errorf("state/ip = %s/%q, want %s/%q", got.State, got.IP, tc.wantState, tc.wantIP)
|
|
}
|
|
if got.UID != "uid-1" {
|
|
t.Errorf("UID = %q, want uid-1 (from the GC label)", got.UID)
|
|
}
|
|
if got.ID != "zones/europe-west1-b/instances/proxy-abc" {
|
|
t.Errorf("ID = %s, want the zone-qualified providerID", got.ID)
|
|
}
|
|
if got.CreatedAt.IsZero() {
|
|
t.Error("CreatedAt not parsed from creationTimestamp")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGet_notFound(t *testing.T) {
|
|
t.Parallel()
|
|
p := newTestProvider(&fakeAPI{getErr: gerr(404)})
|
|
_, err := p.Get(context.Background(), "zones/z/instances/gone")
|
|
if !errors.Is(err, provider.ErrNotFound) {
|
|
t.Errorf("err = %v, want ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestGet_malformedProviderID(t *testing.T) {
|
|
t.Parallel()
|
|
p := newTestProvider(&fakeAPI{})
|
|
for _, id := range []string{"", "proxy-abc", "zones//instances/x", "zones/z/instances/", "z/zone/i/name"} {
|
|
if _, err := p.Get(context.Background(), id); provider.Class(err) != provider.ErrPermanent {
|
|
t.Errorf("Get(%q): Class = %v, want ErrPermanent", id, provider.Class(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDelete_notFoundIsSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
api := &fakeAPI{deleteErr: gerr(404)}
|
|
p := newTestProvider(api)
|
|
if err := p.Delete(context.Background(), "zones/z/instances/gone"); err != nil {
|
|
t.Errorf("Delete of missing instance: %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestDelete_sendsParsedZoneAndName(t *testing.T) {
|
|
t.Parallel()
|
|
api := &fakeAPI{}
|
|
p := newTestProvider(api)
|
|
if err := p.Delete(context.Background(), "zones/us-east1-c/instances/proxy-xyz"); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
if api.deleteReq.Zone != "us-east1-c" || api.deleteReq.Instance != "proxy-xyz" {
|
|
t.Errorf("delete sent %s/%s, want us-east1-c/proxy-xyz", api.deleteReq.Zone, api.deleteReq.Instance)
|
|
}
|
|
}
|
|
|
|
func TestListByTag(t *testing.T) {
|
|
t.Parallel()
|
|
api := &fakeAPI{listInsts: []*computepb.Instance{{
|
|
Name: proto.String("proxy-old"),
|
|
Status: proto.String("RUNNING"),
|
|
Zone: proto.String("https://www.googleapis.com/compute/v1/projects/my-project/zones/europe-west1-b"),
|
|
CreationTimestamp: proto.String(time.Now().Format(time.RFC3339)),
|
|
Labels: map[string]string{
|
|
provider.LabelManaged: provider.LabelManagedYes,
|
|
provider.LabelUID: "uid-orphan",
|
|
},
|
|
NetworkInterfaces: []*computepb.NetworkInterface{{
|
|
AccessConfigs: []*computepb.AccessConfig{{NatIP: proto.String("34.9.9.9")}},
|
|
}},
|
|
}}}
|
|
p := newTestProvider(api)
|
|
|
|
got, err := p.ListByTag(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("ListByTag: %v", err)
|
|
}
|
|
|
|
if want := "labels.proxy-operator-managed = true"; api.listReq.GetFilter() != want {
|
|
t.Errorf("filter = %q, want %q", api.listReq.GetFilter(), want)
|
|
}
|
|
if !api.listReq.GetReturnPartialSuccess() {
|
|
t.Error("ReturnPartialSuccess not set — one unreachable zone would fail the whole GC sweep")
|
|
}
|
|
|
|
if len(got) != 1 {
|
|
t.Fatalf("instances = %d, want 1", len(got))
|
|
}
|
|
if got[0].ID != "zones/europe-west1-b/instances/proxy-old" {
|
|
t.Errorf("ID = %s, want the zone parsed out of the URL-style zone field", got[0].ID)
|
|
}
|
|
if got[0].UID != "uid-orphan" || got[0].State != provider.StateRunning {
|
|
t.Errorf("instance = %+v, want uid-orphan/Running", got[0])
|
|
}
|
|
}
|
|
|
|
func TestListByTag_errorPropagates(t *testing.T) {
|
|
t.Parallel()
|
|
p := newTestProvider(&fakeAPI{listErr: gerr(500)})
|
|
_, err := p.ListByTag(context.Background())
|
|
if provider.Class(err) != provider.ErrTransient {
|
|
t.Errorf("Class = %v, want ErrTransient", provider.Class(err))
|
|
}
|
|
}
|
|
|
|
func TestParseProviderID_roundTrip(t *testing.T) {
|
|
t.Parallel()
|
|
id := formatProviderID("europe-west1-b", "proxy-abc")
|
|
zone, name, err := parseProviderID(id)
|
|
if err != nil || zone != "europe-west1-b" || name != "proxy-abc" {
|
|
t.Errorf("round trip = %s/%s (%v), want europe-west1-b/proxy-abc", zone, name, err)
|
|
}
|
|
}
|