ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
2 changed files with 145336 additions and 0 deletions
256
services/short/cloud/oci.go
Normal file
256
services/short/cloud/oci.go
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/oracle/oci-go-sdk/v65/common"
|
||||
"github.com/oracle/oci-go-sdk/v65/core"
|
||||
"quorate.dev/services/short/config"
|
||||
)
|
||||
|
||||
type Network interface {
|
||||
ListCpes(context.Context, core.ListCpesRequest) (core.ListCpesResponse, error)
|
||||
CreateCpe(context.Context, core.CreateCpeRequest) (core.CreateCpeResponse, error)
|
||||
DeleteCpe(context.Context, core.DeleteCpeRequest) (core.DeleteCpeResponse, error)
|
||||
ListIPSecConnections(context.Context, core.ListIPSecConnectionsRequest) (core.ListIPSecConnectionsResponse, error)
|
||||
CreateIPSecConnection(context.Context, core.CreateIPSecConnectionRequest) (core.CreateIPSecConnectionResponse, error)
|
||||
UpdateIPSecConnection(context.Context, core.UpdateIPSecConnectionRequest) (core.UpdateIPSecConnectionResponse, error)
|
||||
DeleteIPSecConnection(context.Context, core.DeleteIPSecConnectionRequest) (core.DeleteIPSecConnectionResponse, error)
|
||||
ListIPSecConnectionTunnels(context.Context, core.ListIPSecConnectionTunnelsRequest) (core.ListIPSecConnectionTunnelsResponse, error)
|
||||
UpdateIPSecConnectionTunnel(context.Context, core.UpdateIPSecConnectionTunnelRequest) (core.UpdateIPSecConnectionTunnelResponse, error)
|
||||
GetIPSecConnectionTunnelSharedSecret(context.Context, core.GetIPSecConnectionTunnelSharedSecretRequest) (core.GetIPSecConnectionTunnelSharedSecretResponse, error)
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
API Network
|
||||
Config config.Cloud
|
||||
}
|
||||
type State struct {
|
||||
Bundle config.Bundle
|
||||
Peers []string
|
||||
Ready bool
|
||||
}
|
||||
|
||||
func New(c config.Cloud, key string) (*Client, error) {
|
||||
p := common.NewRawConfigurationProvider(c.Tenancy, c.User, c.Region, c.Fingerprint, key, nil)
|
||||
api, err := core.NewVirtualNetworkClientWithConfigurationProvider(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Client{API: &api, Config: c}, nil
|
||||
}
|
||||
|
||||
func token(kind, drg, wan string) *string {
|
||||
sum := sha256.Sum256([]byte(kind + ":" + drg + ":" + wan))
|
||||
value := hex.EncodeToString(sum[:])
|
||||
return &value
|
||||
}
|
||||
|
||||
func create[T any](retry *string, call func(*string) (T, error)) (T, error) {
|
||||
for {
|
||||
result, err := call(retry)
|
||||
service, ok := common.IsServiceError(err)
|
||||
if !ok || service.GetHTTPStatusCode() != 409 || service.GetCode() != "InvalidatedRetryToken" {
|
||||
return result, err
|
||||
}
|
||||
// Deterministic successors let another leader replay a create whose response was lost.
|
||||
sum := sha256.Sum256([]byte(*retry))
|
||||
retry = common.String(hex.EncodeToString(sum[:]))
|
||||
}
|
||||
}
|
||||
|
||||
func owned(tags map[string]string) bool { return tags["owner"] == "short" }
|
||||
|
||||
func (c *Client) Reconcile(ctx context.Context, wan string) (State, error) {
|
||||
state := State{Bundle: config.Bundle{WAN: wan}}
|
||||
var cpes []core.Cpe
|
||||
var page *string
|
||||
for {
|
||||
r, err := c.API.ListCpes(ctx, core.ListCpesRequest{CompartmentId: &c.Config.Compartment, Page: page})
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
for _, p := range r.Items {
|
||||
if owned(p.FreeformTags) {
|
||||
cpes = append(cpes, p)
|
||||
}
|
||||
}
|
||||
if r.OpcNextPage == nil {
|
||||
break
|
||||
}
|
||||
page = r.OpcNextPage
|
||||
}
|
||||
var current *core.Cpe
|
||||
for i := range cpes {
|
||||
if *cpes[i].IpAddress == wan {
|
||||
if current != nil {
|
||||
return state, fmt.Errorf("duplicate owned CPE for WAN")
|
||||
}
|
||||
current = &cpes[i]
|
||||
}
|
||||
}
|
||||
if current == nil {
|
||||
r, err := create(token("cpe", c.Config.DRG, wan), func(retry *string) (core.CreateCpeResponse, error) {
|
||||
return c.API.CreateCpe(ctx, core.CreateCpeRequest{OpcRetryToken: retry, CreateCpeDetails: core.CreateCpeDetails{
|
||||
CompartmentId: &c.Config.Compartment, IpAddress: &wan, DisplayName: common.String("short"), FreeformTags: map[string]string{"owner": "short"},
|
||||
}})
|
||||
})
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
current = &r.Cpe
|
||||
}
|
||||
var connections []core.IpSecConnection
|
||||
page = nil
|
||||
for {
|
||||
r, err := c.API.ListIPSecConnections(ctx, core.ListIPSecConnectionsRequest{CompartmentId: &c.Config.Compartment, DrgId: &c.Config.DRG, Page: page})
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
for _, p := range r.Items {
|
||||
if owned(p.FreeformTags) {
|
||||
connections = append(connections, p)
|
||||
}
|
||||
}
|
||||
if r.OpcNextPage == nil {
|
||||
break
|
||||
}
|
||||
page = r.OpcNextPage
|
||||
}
|
||||
var connection *core.IpSecConnection
|
||||
for i := range connections {
|
||||
p := &connections[i]
|
||||
if *p.CpeId == *current.Id && p.LifecycleState != core.IpSecConnectionLifecycleStateTerminated && p.LifecycleState != core.IpSecConnectionLifecycleStateTerminating {
|
||||
if connection != nil {
|
||||
return state, fmt.Errorf("duplicate owned IPsec connection")
|
||||
}
|
||||
connection = p
|
||||
}
|
||||
}
|
||||
if connection == nil {
|
||||
r, err := create(token("ipsec", c.Config.DRG, wan), func(retry *string) (core.CreateIPSecConnectionResponse, error) {
|
||||
return c.API.CreateIPSecConnection(ctx, core.CreateIPSecConnectionRequest{OpcRetryToken: retry, CreateIpSecConnectionDetails: core.CreateIpSecConnectionDetails{
|
||||
CompartmentId: &c.Config.Compartment, CpeId: current.Id, DrgId: &c.Config.DRG, DisplayName: common.String("short"),
|
||||
FreeformTags: map[string]string{"owner": "short"}, StaticRoutes: []string{c.Config.Address + "/32"},
|
||||
CpeLocalIdentifier: &wan, CpeLocalIdentifierType: core.CreateIpSecConnectionDetailsCpeLocalIdentifierTypeIpAddress,
|
||||
TunnelConfiguration: []core.CreateIpSecConnectionTunnelDetails{tunnel("short-0"), tunnel("short-1")},
|
||||
}})
|
||||
})
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
connection = &r.IpSecConnection
|
||||
}
|
||||
routes := []string{c.Config.Address + "/32"}
|
||||
routeReady := slices.Equal(connection.StaticRoutes, routes)
|
||||
if connection.LifecycleState == core.IpSecConnectionLifecycleStateAvailable && !routeReady {
|
||||
if _, err := c.API.UpdateIPSecConnection(ctx, core.UpdateIPSecConnectionRequest{
|
||||
IpscId: connection.Id, UpdateIpSecConnectionDetails: core.UpdateIpSecConnectionDetails{StaticRoutes: routes},
|
||||
}); err != nil {
|
||||
return state, err
|
||||
}
|
||||
}
|
||||
allUp := false
|
||||
for _, p := range append(connectionsWithout(connections, *connection.Id), *connection) {
|
||||
if p.LifecycleState != core.IpSecConnectionLifecycleStateAvailable {
|
||||
continue
|
||||
}
|
||||
r, err := c.API.ListIPSecConnectionTunnels(ctx, core.ListIPSecConnectionTunnelsRequest{IpscId: p.Id})
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
isCurrent := *p.Id == *connection.Id
|
||||
up := len(r.Items) == 2
|
||||
for _, head := range r.Items {
|
||||
if head.VpnIp != nil {
|
||||
state.Peers = append(state.Peers, *head.VpnIp+"/32")
|
||||
}
|
||||
if head.LifecycleState != core.IpSecConnectionTunnelLifecycleStateAvailable {
|
||||
up = false
|
||||
continue
|
||||
}
|
||||
if head.VpnIp == nil {
|
||||
up = false
|
||||
continue
|
||||
}
|
||||
if isCurrent && head.OracleCanInitiate != core.IpSecConnectionTunnelOracleCanInitiateInitiatorOrResponder {
|
||||
_, err := c.API.UpdateIPSecConnectionTunnel(ctx, core.UpdateIPSecConnectionTunnelRequest{
|
||||
IpscId: p.Id, TunnelId: head.Id,
|
||||
UpdateIpSecConnectionTunnelDetails: core.UpdateIpSecConnectionTunnelDetails{
|
||||
OracleInitiation: core.UpdateIpSecConnectionTunnelDetailsOracleInitiationInitiatorOrResponder,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
}
|
||||
if head.Status != core.IpSecConnectionTunnelStatusUp {
|
||||
up = false
|
||||
}
|
||||
if isCurrent {
|
||||
secret, err := c.API.GetIPSecConnectionTunnelSharedSecret(ctx, core.GetIPSecConnectionTunnelSharedSecretRequest{IpscId: p.Id, TunnelId: head.Id})
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
state.Bundle.Peers = append(state.Bundle.Peers, config.Peer{ID: *head.Id, Address: *head.VpnIp, Secret: *secret.SharedSecret})
|
||||
}
|
||||
}
|
||||
if isCurrent {
|
||||
allUp = up
|
||||
}
|
||||
}
|
||||
sort.Slice(state.Bundle.Peers, func(i, j int) bool { return state.Bundle.Peers[i].ID < state.Bundle.Peers[j].ID })
|
||||
sort.Strings(state.Peers)
|
||||
state.Ready = len(state.Bundle.Peers) == 2 && routeReady
|
||||
if state.Ready && allUp {
|
||||
for _, p := range connections {
|
||||
if *p.Id != *connection.Id && p.LifecycleState != core.IpSecConnectionLifecycleStateTerminating && p.LifecycleState != core.IpSecConnectionLifecycleStateTerminated {
|
||||
if _, err := c.API.DeleteIPSecConnection(ctx, core.DeleteIPSecConnectionRequest{IpscId: p.Id}); err != nil {
|
||||
return state, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range cpes {
|
||||
if *p.Id == *current.Id {
|
||||
continue
|
||||
}
|
||||
used := false
|
||||
for _, conn := range connections {
|
||||
if *conn.CpeId == *p.Id && conn.LifecycleState != core.IpSecConnectionLifecycleStateTerminated {
|
||||
used = true
|
||||
}
|
||||
}
|
||||
if !used {
|
||||
if _, err := c.API.DeleteCpe(ctx, core.DeleteCpeRequest{CpeId: p.Id}); err != nil {
|
||||
return state, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func connectionsWithout(all []core.IpSecConnection, id string) []core.IpSecConnection {
|
||||
var result []core.IpSecConnection
|
||||
for _, p := range all {
|
||||
if *p.Id != id {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func tunnel(name string) core.CreateIpSecConnectionTunnelDetails {
|
||||
return core.CreateIpSecConnectionTunnelDetails{
|
||||
DisplayName: &name, Routing: core.CreateIpSecConnectionTunnelDetailsRoutingStatic, IkeVersion: core.CreateIpSecConnectionTunnelDetailsIkeVersionV2,
|
||||
OracleInitiation: core.CreateIpSecConnectionTunnelDetailsOracleInitiationInitiatorOrResponder, NatTranslationEnabled: core.CreateIpSecConnectionTunnelDetailsNatTranslationEnabledAuto,
|
||||
PhaseOneConfig: &core.PhaseOneConfigDetails{IsCustomPhaseOneConfig: common.Bool(true), EncryptionAlgorithm: core.PhaseOneConfigDetailsEncryptionAlgorithm256Cbc, AuthenticationAlgorithm: core.PhaseOneConfigDetailsAuthenticationAlgorithmSha2256, DiffieHelmanGroup: core.PhaseOneConfigDetailsDiffieHelmanGroupGroup14, LifetimeInSeconds: common.Int(28800)},
|
||||
PhaseTwoConfig: &core.PhaseTwoConfigDetails{IsCustomPhaseTwoConfig: common.Bool(true), EncryptionAlgorithm: core.PhaseTwoConfigDetailsEncryptionAlgorithm256Gcm, IsPfsEnabled: common.Bool(true), PfsDhGroup: core.PhaseTwoConfigDetailsPfsDhGroupGroup14, LifetimeInSeconds: common.Int(3600)},
|
||||
DpdConfig: &core.DpdConfig{DpdMode: core.DpdConfigDpdModeInitiateAndRespond, DpdTimeoutInSec: common.Int(20)},
|
||||
}
|
||||
}
|
||||
383
services/short/cloud/oci_test.go
Normal file
383
services/short/cloud/oci_test.go
Normal file
|
|
@ -0,0 +1,383 @@
|
|||
package cloud
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/oracle/oci-go-sdk/v65/common"
|
||||
"github.com/oracle/oci-go-sdk/v65/core"
|
||||
"quorate.dev/services/short/config"
|
||||
)
|
||||
|
||||
type networkFake struct {
|
||||
Network
|
||||
Cpes []core.Cpe
|
||||
Connections []core.IpSecConnection
|
||||
Heads map[string][]core.IpSecConnectionTunnel
|
||||
Creates, Deletes, Updates int
|
||||
RouteUpdates int
|
||||
HoldRoutes bool
|
||||
PendingRoutes []string
|
||||
}
|
||||
|
||||
func (f *networkFake) ListCpes(context.Context, core.ListCpesRequest) (core.ListCpesResponse, error) {
|
||||
return core.ListCpesResponse{Items: f.Cpes}, nil
|
||||
}
|
||||
func (f *networkFake) CreateCpe(_ context.Context, r core.CreateCpeRequest) (core.CreateCpeResponse, error) {
|
||||
f.Creates++
|
||||
p := core.Cpe{Id: common.String(fmt.Sprintf("cpe-%d", f.Creates)), IpAddress: r.IpAddress, FreeformTags: r.FreeformTags}
|
||||
f.Cpes = append(f.Cpes, p)
|
||||
return core.CreateCpeResponse{Cpe: p}, nil
|
||||
}
|
||||
func (f *networkFake) DeleteCpe(_ context.Context, r core.DeleteCpeRequest) (core.DeleteCpeResponse, error) {
|
||||
f.Deletes++
|
||||
var keep []core.Cpe
|
||||
for _, p := range f.Cpes {
|
||||
if *p.Id != *r.CpeId {
|
||||
keep = append(keep, p)
|
||||
}
|
||||
}
|
||||
f.Cpes = keep
|
||||
return core.DeleteCpeResponse{}, nil
|
||||
}
|
||||
func (f *networkFake) ListIPSecConnections(context.Context, core.ListIPSecConnectionsRequest) (core.ListIPSecConnectionsResponse, error) {
|
||||
return core.ListIPSecConnectionsResponse{Items: f.Connections}, nil
|
||||
}
|
||||
func (f *networkFake) CreateIPSecConnection(_ context.Context, r core.CreateIPSecConnectionRequest) (core.CreateIPSecConnectionResponse, error) {
|
||||
f.Creates++
|
||||
id := fmt.Sprintf("conn-%d", f.Creates)
|
||||
p := core.IpSecConnection{Id: &id, CpeId: r.CpeId, DrgId: r.DrgId, FreeformTags: r.FreeformTags, StaticRoutes: r.StaticRoutes, CpeLocalIdentifier: r.CpeLocalIdentifier, LifecycleState: core.IpSecConnectionLifecycleStateAvailable}
|
||||
f.Connections = append(f.Connections, p)
|
||||
for i, head := range r.TunnelConfiguration {
|
||||
f.Heads[id] = append(f.Heads[id], core.IpSecConnectionTunnel{Id: common.String(fmt.Sprintf("%s-%d", id, i)), VpnIp: common.String(fmt.Sprintf("8.8.%d.%d", f.Creates, i+1)), LifecycleState: core.IpSecConnectionTunnelLifecycleStateAvailable, Status: core.IpSecConnectionTunnelStatusDown, IkeVersion: core.IpSecConnectionTunnelIkeVersionEnum(head.IkeVersion), OracleCanInitiate: core.IpSecConnectionTunnelOracleCanInitiateEnum(head.OracleInitiation)})
|
||||
}
|
||||
return core.CreateIPSecConnectionResponse{IpSecConnection: p}, nil
|
||||
}
|
||||
func (f *networkFake) DeleteIPSecConnection(_ context.Context, r core.DeleteIPSecConnectionRequest) (core.DeleteIPSecConnectionResponse, error) {
|
||||
f.Deletes++
|
||||
var keep []core.IpSecConnection
|
||||
for _, p := range f.Connections {
|
||||
if *p.Id != *r.IpscId {
|
||||
keep = append(keep, p)
|
||||
}
|
||||
}
|
||||
f.Connections = keep
|
||||
return core.DeleteIPSecConnectionResponse{}, nil
|
||||
}
|
||||
func (f *networkFake) UpdateIPSecConnection(_ context.Context, r core.UpdateIPSecConnectionRequest) (core.UpdateIPSecConnectionResponse, error) {
|
||||
f.RouteUpdates++
|
||||
f.PendingRoutes = slices.Clone(r.StaticRoutes)
|
||||
for i, p := range f.Connections {
|
||||
if *p.Id == *r.IpscId {
|
||||
p.StaticRoutes = slices.Clone(r.StaticRoutes)
|
||||
if !f.HoldRoutes {
|
||||
f.Connections[i] = p
|
||||
}
|
||||
return core.UpdateIPSecConnectionResponse{IpSecConnection: p}, nil
|
||||
}
|
||||
}
|
||||
return core.UpdateIPSecConnectionResponse{}, fmt.Errorf("unknown connection")
|
||||
}
|
||||
func (f *networkFake) ListIPSecConnectionTunnels(_ context.Context, r core.ListIPSecConnectionTunnelsRequest) (core.ListIPSecConnectionTunnelsResponse, error) {
|
||||
return core.ListIPSecConnectionTunnelsResponse{Items: f.Heads[*r.IpscId]}, nil
|
||||
}
|
||||
func (f *networkFake) UpdateIPSecConnectionTunnel(_ context.Context, r core.UpdateIPSecConnectionTunnelRequest) (core.UpdateIPSecConnectionTunnelResponse, error) {
|
||||
f.Updates++
|
||||
for i := range f.Heads[*r.IpscId] {
|
||||
p := &f.Heads[*r.IpscId][i]
|
||||
if *p.Id == *r.TunnelId {
|
||||
p.OracleCanInitiate = core.IpSecConnectionTunnelOracleCanInitiateEnum(r.OracleInitiation)
|
||||
}
|
||||
}
|
||||
return core.UpdateIPSecConnectionTunnelResponse{}, nil
|
||||
}
|
||||
func (f *networkFake) GetIPSecConnectionTunnelSharedSecret(context.Context, core.GetIPSecConnectionTunnelSharedSecretRequest) (core.GetIPSecConnectionTunnelSharedSecretResponse, error) {
|
||||
return core.GetIPSecConnectionTunnelSharedSecretResponse{IpSecConnectionTunnelSharedSecret: core.IpSecConnectionTunnelSharedSecret{SharedSecret: common.String("synthetic-secret")}}, nil
|
||||
}
|
||||
|
||||
func TestWANReplacement(t *testing.T) {
|
||||
f := &networkFake{Heads: map[string][]core.IpSecConnectionTunnel{}, Cpes: []core.Cpe{{Id: common.String("foreign"), IpAddress: common.String("8.8.4.4"), FreeformTags: map[string]string{"owner": "relay"}}}}
|
||||
c := Client{API: f, Config: config.Cloud{Compartment: "synthetic-compartment", DRG: "synthetic-drg", Address: "10.76.79.10"}}
|
||||
ctx := context.Background()
|
||||
first, err := c.Reconcile(ctx, "8.8.4.4")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !first.Ready || len(first.Peers) != 2 || f.Creates != 2 {
|
||||
t.Fatal("initial headend publication failed")
|
||||
}
|
||||
for _, head := range f.Heads[*f.Connections[0].Id] {
|
||||
if head.OracleCanInitiate != core.IpSecConnectionTunnelOracleCanInitiateInitiatorOrResponder {
|
||||
t.Fatal("OCI initiation disabled")
|
||||
}
|
||||
}
|
||||
if _, err = c.Reconcile(ctx, "8.8.4.4"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.Creates != 2 || f.Deletes != 0 {
|
||||
t.Fatal("steady reconciliation mutated cloud resources")
|
||||
}
|
||||
next, err := c.Reconcile(ctx, "8.8.8.8")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if next.Bundle.WAN != "8.8.8.8" || len(next.Peers) != 4 || f.Creates != 4 || f.Deletes != 0 {
|
||||
t.Fatal("replacement did not retain prior peers until new tunnels established")
|
||||
}
|
||||
current := f.Connections[1]
|
||||
if *current.CpeLocalIdentifier != "8.8.8.8" || len(current.StaticRoutes) != 1 || current.StaticRoutes[0] != "10.76.79.10/32" {
|
||||
t.Fatal("replacement retained old WAN identity or node routing")
|
||||
}
|
||||
heads := f.Heads[*current.Id]
|
||||
for i := range heads {
|
||||
heads[i].Status = core.IpSecConnectionTunnelStatusUp
|
||||
}
|
||||
f.Heads[*current.Id] = heads
|
||||
if _, err = c.Reconcile(ctx, "8.8.8.8"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.Deletes != 1 {
|
||||
t.Fatal("old connection not retired after both new headends became healthy")
|
||||
}
|
||||
if _, err = c.Reconcile(ctx, "8.8.8.8"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.Deletes != 2 || len(f.Cpes) != 2 || *f.Cpes[0].Id != "foreign" {
|
||||
t.Fatal("CPE retirement affected another ingress owner")
|
||||
}
|
||||
steady, err := c.Reconcile(ctx, "8.8.8.8")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(steady.Peers) != 2 || f.Creates != 4 || f.Deletes != 2 {
|
||||
t.Fatal("obsolete peer allowlist survived replacement")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitiationDrift(t *testing.T) {
|
||||
f := &networkFake{Heads: map[string][]core.IpSecConnectionTunnel{}}
|
||||
c := Client{API: f, Config: config.Cloud{Compartment: "synthetic", DRG: "synthetic", Address: "10.76.79.10"}}
|
||||
ctx := context.Background()
|
||||
if _, err := c.Reconcile(ctx, "8.8.4.4"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id := *f.Connections[0].Id
|
||||
f.Heads[id][0].OracleCanInitiate = core.IpSecConnectionTunnelOracleCanInitiateResponderOnly
|
||||
if _, err := c.Reconcile(ctx, "8.8.4.4"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.Updates != 1 {
|
||||
t.Fatal("initiation drift not reconciled")
|
||||
}
|
||||
if _, err := c.Reconcile(ctx, "8.8.4.4"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.Updates != 1 {
|
||||
t.Fatal("unchanged initiation rewritten")
|
||||
}
|
||||
}
|
||||
|
||||
type apiError struct {
|
||||
Status int
|
||||
Code string
|
||||
}
|
||||
|
||||
func (e apiError) Error() string { return e.Code }
|
||||
func (e apiError) GetHTTPStatusCode() int { return e.Status }
|
||||
func (e apiError) GetCode() string { return e.Code }
|
||||
func (e apiError) GetMessage() string { return e.Code }
|
||||
func (e apiError) GetOpcRequestID() string {
|
||||
return "synthetic-request"
|
||||
}
|
||||
|
||||
type retryFake struct {
|
||||
*networkFake
|
||||
CpeTokens map[string]string
|
||||
IPSecTokens map[string]string
|
||||
LoseKind string
|
||||
LostToken string
|
||||
HiddenCpe string
|
||||
HiddenIPSec string
|
||||
Replayed []string
|
||||
Invalid int
|
||||
}
|
||||
|
||||
func retryNetwork() *retryFake {
|
||||
return &retryFake{
|
||||
networkFake: &networkFake{Heads: map[string][]core.IpSecConnectionTunnel{}},
|
||||
CpeTokens: map[string]string{}, IPSecTokens: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *retryFake) ListCpes(context.Context, core.ListCpesRequest) (core.ListCpesResponse, error) {
|
||||
var result core.ListCpesResponse
|
||||
for _, p := range f.Cpes {
|
||||
if *p.Id != f.HiddenCpe {
|
||||
result.Items = append(result.Items, p)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (f *retryFake) ListIPSecConnections(context.Context, core.ListIPSecConnectionsRequest) (core.ListIPSecConnectionsResponse, error) {
|
||||
var result core.ListIPSecConnectionsResponse
|
||||
for _, p := range f.Connections {
|
||||
if *p.Id != f.HiddenIPSec {
|
||||
result.Items = append(result.Items, p)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (f *retryFake) CreateCpe(ctx context.Context, r core.CreateCpeRequest) (core.CreateCpeResponse, error) {
|
||||
if id, ok := f.CpeTokens[*r.OpcRetryToken]; ok {
|
||||
for _, p := range f.Cpes {
|
||||
if *p.Id == id {
|
||||
f.Replayed = append(f.Replayed, *r.OpcRetryToken)
|
||||
return core.CreateCpeResponse{Cpe: p}, nil
|
||||
}
|
||||
}
|
||||
f.Invalid++
|
||||
return core.CreateCpeResponse{}, apiError{409, "InvalidatedRetryToken"}
|
||||
}
|
||||
result, err := f.networkFake.CreateCpe(ctx, r)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
f.CpeTokens[*r.OpcRetryToken] = *result.Id
|
||||
if f.LoseKind == "cpe" {
|
||||
f.LoseKind, f.LostToken, f.HiddenCpe = "", *r.OpcRetryToken, *result.Id
|
||||
return core.CreateCpeResponse{}, context.DeadlineExceeded
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (f *retryFake) CreateIPSecConnection(ctx context.Context, r core.CreateIPSecConnectionRequest) (core.CreateIPSecConnectionResponse, error) {
|
||||
if id, ok := f.IPSecTokens[*r.OpcRetryToken]; ok {
|
||||
for _, p := range f.Connections {
|
||||
if *p.Id == id {
|
||||
f.Replayed = append(f.Replayed, *r.OpcRetryToken)
|
||||
return core.CreateIPSecConnectionResponse{IpSecConnection: p}, nil
|
||||
}
|
||||
}
|
||||
f.Invalid++
|
||||
return core.CreateIPSecConnectionResponse{}, apiError{409, "InvalidatedRetryToken"}
|
||||
}
|
||||
result, err := f.networkFake.CreateIPSecConnection(ctx, r)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
f.IPSecTokens[*r.OpcRetryToken] = *result.Id
|
||||
if f.LoseKind == "ipsec" {
|
||||
f.LoseKind, f.LostToken, f.HiddenIPSec = "", *r.OpcRetryToken, *result.Id
|
||||
return core.CreateIPSecConnectionResponse{}, context.DeadlineExceeded
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func settleWAN(t *testing.T, c *Client, f *retryFake, wan string) {
|
||||
t.Helper()
|
||||
if state, err := c.Reconcile(context.Background(), wan); err != nil || !state.Ready {
|
||||
t.Fatalf("WAN %s: ready=%v error=%v", wan, state.Ready, err)
|
||||
}
|
||||
for _, heads := range f.Heads {
|
||||
for i := range heads {
|
||||
heads[i].Status = core.IpSecConnectionTunnelStatusUp
|
||||
}
|
||||
}
|
||||
for range 2 {
|
||||
if _, err := c.Reconcile(context.Background(), wan); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWANReturn(t *testing.T) {
|
||||
f := retryNetwork()
|
||||
c := Client{API: f, Config: config.Cloud{Compartment: "synthetic", DRG: "synthetic", Address: "192.0.2.10"}}
|
||||
for _, wan := range []string{"198.51.100.10", "198.51.100.20", "198.51.100.10"} {
|
||||
settleWAN(t, &c, f, wan)
|
||||
}
|
||||
if f.Creates != 6 || f.Deletes != 4 || f.Invalid != 2 || len(f.Cpes) != 1 || len(f.Connections) != 1 {
|
||||
t.Fatalf("WAN return failed to retire and replace: creates=%d deletes=%d invalid=%d CPEs=%d connections=%d", f.Creates, f.Deletes, f.Invalid, len(f.Cpes), len(f.Connections))
|
||||
}
|
||||
if len(f.CpeTokens) != 3 || len(f.IPSecTokens) != 3 {
|
||||
t.Fatal("distinct resource generations reused a creation token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTimeout(t *testing.T) {
|
||||
for _, kind := range []string{"cpe", "ipsec"} {
|
||||
t.Run(kind, func(t *testing.T) {
|
||||
f := retryNetwork()
|
||||
cfg := config.Cloud{Compartment: "synthetic", DRG: "synthetic", Address: "192.0.2.10"}
|
||||
c := Client{API: f, Config: cfg}
|
||||
settleWAN(t, &c, f, "198.51.100.10")
|
||||
settleWAN(t, &c, f, "198.51.100.20")
|
||||
f.LoseKind = kind
|
||||
if _, err := c.Reconcile(context.Background(), "198.51.100.10"); !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("create did not commit with a lost reply: %v", err)
|
||||
}
|
||||
replacement := Client{API: f, Config: cfg}
|
||||
state, err := replacement.Reconcile(context.Background(), "198.51.100.10")
|
||||
if err != nil || !state.Ready {
|
||||
t.Fatalf("replacement leader failed: ready=%v error=%v", state.Ready, err)
|
||||
}
|
||||
if !slices.Contains(f.Replayed, f.LostToken) || f.Creates != 6 || len(f.Cpes) != 2 || len(f.Connections) != 2 {
|
||||
t.Fatalf("ambiguous create was not replayed: creates=%d CPEs=%d connections=%d", f.Creates, len(f.Cpes), len(f.Connections))
|
||||
}
|
||||
f.HiddenCpe, f.HiddenIPSec = "", ""
|
||||
if _, err := replacement.Reconcile(context.Background(), "198.51.100.10"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.Creates != 6 {
|
||||
t.Fatal("visibility restoration created another resource")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryErrors(t *testing.T) {
|
||||
for _, want := range []error{context.DeadlineExceeded, apiError{409, "IncorrectState"}, apiError{403, "InvalidatedRetryToken"}} {
|
||||
calls := 0
|
||||
_, got := create(common.String("synthetic-token"), func(*string) (int, error) {
|
||||
calls++
|
||||
return 0, want
|
||||
})
|
||||
if calls != 1 || !errors.Is(got, want) {
|
||||
t.Fatalf("unrelated error was retried: %v; calls=%d", got, calls)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteDrift(t *testing.T) {
|
||||
f := &networkFake{Heads: map[string][]core.IpSecConnectionTunnel{}, HoldRoutes: true}
|
||||
c := Client{API: f, Config: config.Cloud{Compartment: "synthetic", DRG: "synthetic", Address: "192.0.2.10"}}
|
||||
ctx := context.Background()
|
||||
if _, err := c.Reconcile(ctx, "198.51.100.10"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Config.Address = "192.0.2.11"
|
||||
for range 2 {
|
||||
state, err := c.Reconcile(ctx, "198.51.100.10")
|
||||
if err != nil || state.Ready || len(state.Peers) != 2 {
|
||||
t.Fatalf("pending route exposed runtime or removed peers: ready=%v peers=%d error=%v", state.Ready, len(state.Peers), err)
|
||||
}
|
||||
}
|
||||
if f.RouteUpdates != 2 || !slices.Equal(f.PendingRoutes, []string{"192.0.2.11/32"}) || f.Connections[0].StaticRoutes[0] != "192.0.2.10/32" {
|
||||
t.Fatal("route update did not target the configured address")
|
||||
}
|
||||
f.Connections[0].StaticRoutes = slices.Clone(f.PendingRoutes)
|
||||
for range 2 {
|
||||
state, err := c.Reconcile(ctx, "198.51.100.10")
|
||||
if err != nil || !state.Ready {
|
||||
t.Fatalf("observed route was not published: ready=%v error=%v", state.Ready, err)
|
||||
}
|
||||
}
|
||||
if f.Creates != 2 || f.Deletes != 0 || f.RouteUpdates != 2 {
|
||||
t.Fatal("route reconciliation replaced resources or repeated a completed update")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue