package controller import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" "sync" "sync/atomic" "testing" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/core" kcore "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" dynfake "k8s.io/client-go/dynamic/fake" "k8s.io/client-go/kubernetes/fake" "quorate.dev/services/short/cloud" "quorate.dev/services/short/config" "quorate.dev/services/short/vault" "quorate.dev/services/short/wan" ) type publishedCloud struct { cloud.Network phase atomic.Int32 } func (f *publishedCloud) generations() []int { switch f.phase.Load() { case 0: return []int{0} case 1: return []int{0, 1} default: return []int{1} } } func (f *publishedCloud) ListCpes(context.Context, core.ListCpesRequest) (core.ListCpesResponse, error) { var result core.ListCpesResponse for _, i := range f.generations() { result.Items = append(result.Items, core.Cpe{Id: common.String(fmt.Sprint(i)), IpAddress: common.String(fmt.Sprintf("192.0.2.%d", i+2)), FreeformTags: map[string]string{"owner": "short"}}) } return result, nil } func (f *publishedCloud) ListIPSecConnections(context.Context, core.ListIPSecConnectionsRequest) (core.ListIPSecConnectionsResponse, error) { var result core.ListIPSecConnectionsResponse for _, i := range f.generations() { result.Items = append(result.Items, core.IpSecConnection{Id: common.String(fmt.Sprint(i)), CpeId: common.String(fmt.Sprint(i)), LifecycleState: core.IpSecConnectionLifecycleStateAvailable, StaticRoutes: []string{"198.18.0.10/32"}, FreeformTags: map[string]string{"owner": "short"}}) } return result, nil } func (f *publishedCloud) ListIPSecConnectionTunnels(_ context.Context, r core.ListIPSecConnectionTunnelsRequest) (core.ListIPSecConnectionTunnelsResponse, error) { var result core.ListIPSecConnectionTunnelsResponse for i := 1; i <= 2; i++ { result.Items = append(result.Items, core.IpSecConnectionTunnel{ Id: common.String(fmt.Sprintf("%s-%d", *r.IpscId, i)), VpnIp: common.String(fmt.Sprintf("192.0.2.%s%d", map[string]string{"0": "1", "1": "2"}[*r.IpscId], i)), LifecycleState: core.IpSecConnectionTunnelLifecycleStateAvailable, Status: core.IpSecConnectionTunnelStatusDown, OracleCanInitiate: core.IpSecConnectionTunnelOracleCanInitiateInitiatorOrResponder, }) } return result, nil } func (f *publishedCloud) GetIPSecConnectionTunnelSharedSecret(_ context.Context, r core.GetIPSecConnectionTunnelSharedSecretRequest) (core.GetIPSecConnectionTunnelSharedSecretResponse, error) { return core.GetIPSecConnectionTunnelSharedSecretResponse{IpSecConnectionTunnelSharedSecret: core.IpSecConnectionTunnelSharedSecret{SharedSecret: common.String("synthetic-" + *r.TunnelId)}}, nil } func TestWANPublication(t *testing.T) { ctx := context.Background() api := &publishedCloud{} dyn := dynfake.NewSimpleDynamicClient(runtime.NewScheme(), &unstructured.Unstructured{Object: map[string]any{ "apiVersion": "cilium.io/v2", "kind": "CiliumCIDRGroup", "metadata": map[string]any{"name": "short"}, "spec": map[string]any{"externalCIDRs": []any{}}, }}) kube := fake.NewClientset(&kcore.Service{ObjectMeta: metav1.ObjectMeta{Name: "cilium-gateway-gateway-short", Namespace: "kube-system"}, Spec: kcore.ServiceSpec{ClusterIP: "192.0.2.100", Ports: []kcore.ServicePort{{Port: 8080}}}}) var mu sync.Mutex version := 1 data := map[string]any{"config": "{}"} writes := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { mu.Lock() defer mu.Unlock() w.Header().Set("Content-Type", "application/json") switch req.URL.Path { case "/api/interfaces/overview/interfaces_info": if req.Method != http.MethodGet { t.Error("WAN reader attempted a mutation") } address := "192.0.2.2" if api.phase.Load() > 0 { address = "192.0.2.3" } fmt.Fprintf(w, `{"rows":[{"identifier":"WAN","ipv4":[{"ipaddr":%q}]}]}`, address) case "/v1/secret/data/short/peers": if req.Method == http.MethodGet { json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"data": data, "metadata": map[string]int{"version": version}}}) return } var input struct { Options struct{ CAS int } `json:"options"` Data map[string]any `json:"data"` } if err := json.NewDecoder(req.Body).Decode(&input); err != nil { t.Error(err) } if req.Method != http.MethodPost || input.Options.CAS != version { t.Error("configuration publication lost its CAS contract") http.Error(w, "conflict", http.StatusBadRequest) return } var published config.Runtime if err := json.Unmarshal([]byte(input.Data["config"].(string)), &published); err != nil { t.Error(err) } current, _ := dyn.Resource(group).Get(ctx, "short", metav1.GetOptions{}) allowed, _, _ := unstructured.NestedStringSlice(current.Object, "spec", "externalCIDRs") for _, p := range published.Peers { found := false for _, cidr := range allowed { found = found || cidr == p.Address+"/32" } if !found { t.Error("published a peer before admitting its address") } } data = input.Data version++ writes++ default: t.Errorf("unexpected request: %s", req.URL.Path) http.NotFound(w, req) } })) defer server.Close() r := Runner{Cloud: &cloud.Client{API: api, Config: config.Cloud{Address: "198.18.0.10", Network: "198.18.1.0/24"}}, WAN: &wan.Client{URL: server.URL, HTTP: server.Client()}, Kube: kube, Dynamic: dyn} kv := &vault.Client{URL: server.URL, HTTP: server.Client(), Token: "synthetic-token"} for phase, wantCIDRs := range []int{2, 4, 2} { api.phase.Store(int32(phase)) if err := r.step(ctx, kv); err != nil { t.Fatal(err) } mu.Lock() value := data["config"].(string) count := writes mu.Unlock() var published config.Runtime if err := json.Unmarshal([]byte(value), &published); err != nil { t.Fatal(err) } wantWAN, wantWrites := "192.0.2.2", 1 if phase > 0 { wantWAN, wantWrites = "192.0.2.3", 2 } if published.WAN != wantWAN || count != wantWrites || published.Path.Address != "198.18.0.10" || published.Path.Network != "198.18.1.0/24" || published.Path.Gateway != "192.0.2.100" || !reflect.DeepEqual(published.Path.Ports, []uint16{8080}) { t.Fatalf("wrong runtime publication: %s; writes=%d", value, count) } current, _ := dyn.Resource(group).Get(ctx, "short", metav1.GetOptions{}) cidrs, _, _ := unstructured.NestedStringSlice(current.Object, "spec", "externalCIDRs") if len(cidrs) != wantCIDRs { t.Fatalf("wrong peer admission transition: %v", cidrs) } if directory := os.Getenv("SHORT_FIXTURE"); directory != "" && phase < 2 { if err := os.WriteFile(filepath.Join(directory, fmt.Sprintf("runtime%d.json", phase)), []byte(value), 0600); err != nil { t.Fatal(err) } } } }