ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
44 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")
|
||||
}
|
||||
}
|
||||
67
services/short/cmd/short/main.go
Normal file
67
services/short/cmd/short/main.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"quorate.dev/services/short/controller"
|
||||
"quorate.dev/services/short/tunnel"
|
||||
"quorate.dev/services/short/vault"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
||||
defer stop()
|
||||
if err := run(ctx, os.Args[1:]); err != nil {
|
||||
slog.Error("short ingress stopped", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("expected controller, tunnel, seed, or stage")
|
||||
}
|
||||
switch args[0] {
|
||||
case "controller":
|
||||
return controller.Run(ctx, "/config", os.Getenv("POD_NAMESPACE"), os.Getenv("POD_UID"), os.Getenv("VAULT_ADDR"), os.Getenv("OPNSENSE_URL"))
|
||||
case "tunnel":
|
||||
return tunnel.Run(ctx, "/config/runtime.json", os.Getenv("POD_NAMESPACE"), os.Getenv("POD_NAME"))
|
||||
case "seed":
|
||||
kv, err := vault.Login(ctx, os.Getenv("VAULT_ADDR"), "short-seed", "/var/run/secrets/kubernetes.io/serviceaccount/token")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return kv.Seed(ctx, "short/api")
|
||||
case "stage":
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("expected binary destination")
|
||||
}
|
||||
file, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
source, err := os.Open(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer source.Close()
|
||||
target, err := os.OpenFile(args[1], os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, copyErr := io.Copy(target, source)
|
||||
closeErr := target.Close()
|
||||
if copyErr != nil {
|
||||
return copyErr
|
||||
}
|
||||
return closeErr
|
||||
default:
|
||||
return fmt.Errorf("unknown command")
|
||||
}
|
||||
}
|
||||
103
services/short/config/config.go
Normal file
103
services/short/config/config.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Peer struct {
|
||||
ID string `json:"id"`
|
||||
Address string `json:"address"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
type Bundle struct {
|
||||
WAN string `json:"wan"`
|
||||
Peers []Peer `json:"peers"`
|
||||
}
|
||||
|
||||
type Runtime struct {
|
||||
Bundle
|
||||
Path Path `json:"path"`
|
||||
}
|
||||
|
||||
type Cloud struct {
|
||||
Tenancy string `json:"tenancy"`
|
||||
User string `json:"user"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
Region string `json:"region"`
|
||||
Compartment string `json:"compartment"`
|
||||
DRG string `json:"drg"`
|
||||
Address string `json:"address"`
|
||||
Network string `json:"network"`
|
||||
}
|
||||
|
||||
type Path struct {
|
||||
Address string `json:"address"`
|
||||
Network string `json:"network"`
|
||||
Gateway string `json:"gateway"`
|
||||
Ports []uint16 `json:"ports"`
|
||||
}
|
||||
|
||||
func Read[T any](path string) (T, error) {
|
||||
var value T
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
err = json.Unmarshal(data, &value)
|
||||
return value, err
|
||||
}
|
||||
|
||||
func (b Bundle) Validate() error {
|
||||
if err := IPv4(b.WAN); err != nil {
|
||||
return fmt.Errorf("WAN: %w", err)
|
||||
}
|
||||
if len(b.Peers) != 2 {
|
||||
return fmt.Errorf("expected two OCI headends")
|
||||
}
|
||||
for _, p := range b.Peers {
|
||||
if p.ID == "" || p.Secret == "" {
|
||||
return fmt.Errorf("incomplete headend")
|
||||
}
|
||||
if err := IPv4(p.Address); err != nil {
|
||||
return fmt.Errorf("headend: %w", err)
|
||||
}
|
||||
}
|
||||
if b.Peers[0].ID == b.Peers[1].ID || b.Peers[0].Address == b.Peers[1].Address {
|
||||
return fmt.Errorf("duplicate headend")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Path) Validate() error {
|
||||
if err := IPv4(p.Address); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := IPv4(p.Gateway); err != nil {
|
||||
return fmt.Errorf("Gateway: %w", err)
|
||||
}
|
||||
n, err := netip.ParsePrefix(p.Network)
|
||||
if err != nil || !n.Addr().Is4() {
|
||||
return fmt.Errorf("invalid OCI network")
|
||||
}
|
||||
if len(p.Ports) == 0 {
|
||||
return fmt.Errorf("empty service allowlist")
|
||||
}
|
||||
for _, port := range p.Ports {
|
||||
if port == 0 {
|
||||
return fmt.Errorf("invalid service port")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func IPv4(value string) error {
|
||||
ip, err := netip.ParseAddr(value)
|
||||
if err != nil || !ip.Is4() || !ip.IsGlobalUnicast() {
|
||||
return fmt.Errorf("invalid unicast IPv4")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
177
services/short/controller/run.go
Normal file
177
services/short/controller/run.go
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"os"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/leaderelection"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
"quorate.dev/services/short/cloud"
|
||||
"quorate.dev/services/short/config"
|
||||
"quorate.dev/services/short/vault"
|
||||
"quorate.dev/services/short/wan"
|
||||
)
|
||||
|
||||
var group = schema.GroupVersionResource{Group: "cilium.io", Version: "v2", Resource: "ciliumcidrgroups"}
|
||||
|
||||
type Runner struct {
|
||||
Cloud *cloud.Client
|
||||
WAN *wan.Client
|
||||
Kube kubernetes.Interface
|
||||
Dynamic dynamic.Interface
|
||||
VaultURL string
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, directory, namespace, identity, vaultURL, opnURL string) error {
|
||||
c, err := config.Read[config.Cloud](directory + "/cloud.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := os.ReadFile(directory + "/private_key")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
oci, err := cloud.New(c, string(key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opn, err := wan.New(opnURL, directory+"/opnsense")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kc, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kube, err := kubernetes.NewForConfig(kc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dyn, err := dynamic.NewForConfig(kc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r := Runner{Cloud: oci, WAN: opn, Kube: kube, Dynamic: dyn, VaultURL: vaultURL}
|
||||
lock := &resourcelock.LeaseLock{LeaseMeta: metav1.ObjectMeta{Name: "controller", Namespace: namespace}, Client: kube.CoordinationV1(), LockConfig: resourcelock.ResourceLockConfig{Identity: identity}}
|
||||
elector, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
|
||||
Lock: lock, LeaseDuration: 30 * time.Second, RenewDeadline: 20 * time.Second, RetryPeriod: 5 * time.Second,
|
||||
ReleaseOnCancel: true, Callbacks: leaderelection.LeaderCallbacks{
|
||||
OnStartedLeading: r.loop,
|
||||
OnStoppedLeading: func() { slog.Info("controller ownership ended") },
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
elector.Run(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Runner) loop(ctx context.Context) {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
var kv *vault.Client
|
||||
var login time.Time
|
||||
for {
|
||||
if kv == nil || time.Since(login) >= 30*time.Minute {
|
||||
client, err := vault.Login(ctx, r.VaultURL, "short-controller", "/var/run/secrets/kubernetes.io/serviceaccount/token")
|
||||
if err != nil {
|
||||
slog.Error("Vault login", "error", err)
|
||||
} else {
|
||||
kv = client
|
||||
login = time.Now()
|
||||
}
|
||||
}
|
||||
if kv != nil {
|
||||
request, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
err := r.step(request, kv)
|
||||
cancel()
|
||||
if err != nil {
|
||||
slog.Error("reconcile short ingress", "error", err)
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) step(ctx context.Context, kv *vault.Client) error {
|
||||
wan, err := r.WAN.Address(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state, err := r.Cloud.Reconcile(ctx, wan)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resource := r.Dynamic.Resource(group)
|
||||
current, err := resource.Get(ctx, "short", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
want := make([]any, len(state.Peers))
|
||||
for i, value := range state.Peers {
|
||||
want[i] = value
|
||||
}
|
||||
got, _, err := unstructured.NestedSlice(current.Object, "spec", "externalCIDRs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
if err := unstructured.SetNestedSlice(current.Object, want, "spec", "externalCIDRs"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = resource.Update(ctx, current, metav1.UpdateOptions{FieldManager: "short"}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !state.Ready {
|
||||
slog.Info("waiting for OCI headends")
|
||||
return nil
|
||||
}
|
||||
svc, err := r.Kube.CoreV1().Services("kube-system").Get(ctx, "cilium-gateway-gateway-short", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ports := make([]uint16, 0, len(svc.Spec.Ports))
|
||||
for _, p := range svc.Spec.Ports {
|
||||
ports = append(ports, uint16(p.Port))
|
||||
}
|
||||
runtime := config.Runtime{Bundle: state.Bundle, Path: config.Path{Address: r.Cloud.Config.Address, Network: r.Cloud.Config.Network, Gateway: svc.Spec.ClusterIP, Ports: ports}}
|
||||
if err = runtime.Bundle.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = runtime.Path.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := json.Marshal(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record, err := kv.Read(ctx, "short/peers")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if record.Data["config"] == string(data) {
|
||||
return nil
|
||||
}
|
||||
record.Data = map[string]any{"config": string(data)}
|
||||
if err = kv.Write(ctx, "short/peers", record); err != nil {
|
||||
return err
|
||||
}
|
||||
slog.Info("published short tunnel configuration", "wan", wan, "headends", len(state.Bundle.Peers), "ports", ports)
|
||||
return nil
|
||||
}
|
||||
177
services/short/controller/run_test.go
Normal file
177
services/short/controller/run_test.go
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
76
services/short/go.mod
Normal file
76
services/short/go.mod
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
module quorate.dev/services/short
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
github.com/google/cel-go v0.26.0
|
||||
github.com/google/nftables v0.3.0
|
||||
github.com/oracle/oci-go-sdk/v65 v65.121.0
|
||||
github.com/strongswan/govici v0.7.0
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
golang.org/x/sys v0.47.0
|
||||
k8s.io/api v0.36.2
|
||||
k8s.io/apimachinery v0.36.2
|
||||
k8s.io/client-go v0.36.2
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v1.0.0 // indirect
|
||||
github.com/go-openapi/jsonreference v1.0.0 // indirect
|
||||
github.com/go-openapi/swag v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/conv v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/loading v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/pools v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.27.1 // indirect
|
||||
github.com/gofrs/flock v0.10.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
||||
github.com/mdlayher/socket v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/sony/gobreaker/v2 v2.4.0 // indirect
|
||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
k8s.io/klog/v2 v2.140.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
|
||||
k8s.io/utils v0.0.0-20260626114624-be93311217bd // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
|
||||
)
|
||||
171
services/short/go.sum
Normal file
171
services/short/go.sum
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
|
||||
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
|
||||
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
|
||||
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
|
||||
github.com/go-openapi/swag v0.27.1 h1:VotvOLWW8q/EAxB0YdsBBGC8XYyeL1YwBj2ungAGPNg=
|
||||
github.com/go-openapi/swag v0.27.1/go.mod h1:GTkJPwHfhJp6MWr4/rCh64HVI3Ofu+tcsbfjfHmTxpE=
|
||||
github.com/go-openapi/swag/cmdutils v0.27.1 h1:I7sYqaWVl5mq0NEmNQkAmFDyNin9ufvMX/p2zwtQaOE=
|
||||
github.com/go-openapi/swag/cmdutils v0.27.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
|
||||
github.com/go-openapi/swag/conv v0.27.1 h1:8wi9ZG+olmY1wXphl93EWniPtbSPkXM/feH7FgjsvrU=
|
||||
github.com/go-openapi/swag/conv v0.27.1/go.mod h1:QbqMivkpKhC3g1B1GGGOJ6ANewI3S62dbzYu3Duowqs=
|
||||
github.com/go-openapi/swag/fileutils v0.27.1 h1:QQqBSoi5mW4XpU85nS0mLcA+zAE6vLzrb0QkmLKf9oM=
|
||||
github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1 h1:mJu3COL9WEaZVp/Kf2PRMi7tPszPEJfSr/OO75ynCs8=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.1/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
|
||||
github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY=
|
||||
github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE=
|
||||
github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA=
|
||||
github.com/go-openapi/swag/mangling v0.27.1/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
|
||||
github.com/go-openapi/swag/netutils v0.27.1 h1:mICMFoS82F5TZ4Zy3cqmcQk+BFeCp3Uyq3Np7GI0/qU=
|
||||
github.com/go-openapi/swag/netutils v0.27.1/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
|
||||
github.com/go-openapi/swag/pools v0.27.1 h1:9LeadcMyb2GJCbXX5hVQDbZ2Lq9TL4dCs/nx1j5DO0E=
|
||||
github.com/go-openapi/swag/pools v0.27.1/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
|
||||
github.com/go-openapi/swag/stringutils v0.27.1 h1:ZXePZ0r2p1qSjo8tD3Un4vFj8+FqlCkczxDrJIhYUp8=
|
||||
github.com/go-openapi/swag/stringutils v0.27.1/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
|
||||
github.com/go-openapi/swag/typeutils v0.27.1 h1:KSTdFlfnse4r6dP9IrEnwMldjE+zs71UeEB3//PtVXc=
|
||||
github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
|
||||
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
|
||||
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||
github.com/gofrs/flock v0.10.0 h1:SHMXenfaB03KbroETaCMtbBg3Yn29v4w1r+tgy4ff4k=
|
||||
github.com/gofrs/flock v0.10.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc=
|
||||
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
|
||||
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg=
|
||||
github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
||||
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
||||
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.121.0 h1:1J+5ARgrodrx8kzFy/hxznaoUzz43jr0EestCzEaOHw=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.121.0/go.mod h1:Pzy+BpgkDesvGZXEHgslwhIYobHCPHg6wRta1mWnlqQ=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sony/gobreaker/v2 v2.4.0 h1:g2KJRW1Ubty3+ZOcSEUN7K+REQJdN6yo6XvaML+jptg=
|
||||
github.com/sony/gobreaker/v2 v2.4.0/go.mod h1:pTyFJgcZ3h2tdQVLZZruK2C0eoFL1fb/G83wK1ZQl+s=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/strongswan/govici v0.7.0 h1:m0BaL5hY+2khnQZYDaKJkd4Ji1jFrNzS0fg+GQS7QnE=
|
||||
github.com/strongswan/govici v0.7.0/go.mod h1:WvC3Lo9kEzjxUb5xNe2B4NczQpa7+cZMIy2x8eZLzSE=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7 h1:YcyjlL1PRr2Q17/I0dPk2JmYS5CDXfcdb2Z3YRioEbw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:OCdP9MfskevB/rbYvHTsXTtKC+3bHWajPdoKgjcYkfo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7 h1:2035KHhUv+EpyB+hWgJnaWKJOdX1E95w2S8Rr4uWKTs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY=
|
||||
k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg=
|
||||
k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ=
|
||||
k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4=
|
||||
k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI=
|
||||
k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0=
|
||||
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A=
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I=
|
||||
k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs=
|
||||
k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
14
services/short/k8s/bootstrap/accounts.yaml
Normal file
14
services/short/k8s/bootstrap/accounts.yaml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: short-vso
|
||||
namespace: short
|
||||
automountServiceAccountToken: false
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: short-seed
|
||||
namespace: short
|
||||
imagePullSecrets:
|
||||
- name: pull
|
||||
55
services/short/k8s/bootstrap/job.yaml
Normal file
55
services/short/k8s/bootstrap/job.yaml
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: seed
|
||||
namespace: short
|
||||
annotations:
|
||||
kustomize.toolkit.fluxcd.io/force: Enabled
|
||||
spec:
|
||||
completions: 2
|
||||
parallelism: 2
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: seed
|
||||
app.kubernetes.io/instance: short
|
||||
spec:
|
||||
serviceAccountName: short-seed
|
||||
restartPolicy: Never
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
fsGroup: 65532
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: seed
|
||||
image: short-image
|
||||
args:
|
||||
- seed
|
||||
env:
|
||||
- name: VAULT_ADDR
|
||||
value: https://vault.vault.svc.cluster.local:8200
|
||||
- name: SSL_CERT_FILE
|
||||
value: /ca/ca.crt
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
memory: 128Mi
|
||||
volumeMounts:
|
||||
- name: ca
|
||||
mountPath: /ca
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: ca
|
||||
secret:
|
||||
secretName: ca
|
||||
9
services/short/k8s/bootstrap/kustomization.yaml
Normal file
9
services/short/k8s/bootstrap/kustomization.yaml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- accounts.yaml
|
||||
- vault.yaml
|
||||
- job.yaml
|
||||
- policy.yaml
|
||||
- vault-access.yaml
|
||||
5
services/short/k8s/bootstrap/namespace.yaml
Normal file
5
services/short/k8s/bootstrap/namespace.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: short
|
||||
labels: {gateway.quorate.internal/allow-short: "true"}
|
||||
31
services/short/k8s/bootstrap/policy.yaml
Normal file
31
services/short/k8s/bootstrap/policy.yaml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
apiVersion: cilium.io/v2
|
||||
kind: CiliumNetworkPolicy
|
||||
metadata:
|
||||
name: seed
|
||||
namespace: short
|
||||
spec:
|
||||
endpointSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: seed
|
||||
egress:
|
||||
- toEndpoints:
|
||||
- matchLabels:
|
||||
k8s:io.kubernetes.pod.namespace: kube-system
|
||||
k8s:k8s-app: kube-dns
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: '53'
|
||||
protocol: UDP
|
||||
- port: '53'
|
||||
protocol: TCP
|
||||
rules:
|
||||
dns:
|
||||
- matchPattern: '*'
|
||||
- toEndpoints:
|
||||
- matchLabels:
|
||||
k8s:io.kubernetes.pod.namespace: vault
|
||||
app.kubernetes.io/name: vault
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: '8200'
|
||||
protocol: TCP
|
||||
17
services/short/k8s/bootstrap/vault-access.yaml
Normal file
17
services/short/k8s/bootstrap/vault-access.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
apiVersion: cilium.io/v2
|
||||
kind: CiliumNetworkPolicy
|
||||
metadata: {name: short, namespace: vault}
|
||||
spec:
|
||||
endpointSelector:
|
||||
matchLabels: {app.kubernetes.io/name: vault, component: server}
|
||||
ingress:
|
||||
- fromEndpoints:
|
||||
- matchLabels:
|
||||
k8s:io.kubernetes.pod.namespace: short
|
||||
app.kubernetes.io/instance: short
|
||||
matchExpressions:
|
||||
- key: app.kubernetes.io/name
|
||||
operator: In
|
||||
values: [seed, controller]
|
||||
toPorts:
|
||||
- ports: [{port: "8200", protocol: TCP}]
|
||||
60
services/short/k8s/bootstrap/vault.yaml
Normal file
60
services/short/k8s/bootstrap/vault.yaml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultAuth
|
||||
metadata:
|
||||
name: short
|
||||
namespace: short
|
||||
spec:
|
||||
method: kubernetes
|
||||
mount: kubernetes
|
||||
kubernetes:
|
||||
role: short-vso
|
||||
serviceAccount: short-vso
|
||||
vaultConnectionRef: vault/vault
|
||||
---
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: pull
|
||||
namespace: short
|
||||
spec:
|
||||
vaultAuthRef: short
|
||||
type: kv-v2
|
||||
mount: secret
|
||||
path: quay/robots/cluster_pull
|
||||
refreshAfter: 1h
|
||||
destination:
|
||||
create: true
|
||||
overwrite: true
|
||||
name: pull
|
||||
type: kubernetes.io/dockerconfigjson
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
excludes:
|
||||
- .*
|
||||
templates:
|
||||
.dockerconfigjson:
|
||||
text: '{"auths":{"registry.quorate.internal":{"auth":"{{ printf "%s:%s"
|
||||
(get .Secrets "username") (get .Secrets "token") | b64enc }}"}}}'
|
||||
---
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultDynamicSecret
|
||||
metadata:
|
||||
name: ca
|
||||
namespace: short
|
||||
spec:
|
||||
vaultAuthRef: short
|
||||
mount: pki
|
||||
path: cert/ca
|
||||
requestHTTPMethod: GET
|
||||
refreshAfter: 1h
|
||||
destination:
|
||||
create: true
|
||||
overwrite: true
|
||||
name: ca
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
excludes:
|
||||
- .*
|
||||
templates:
|
||||
ca.crt:
|
||||
text: '{{ get .Secrets "certificate" }}'
|
||||
114
services/short/k8s/control/deployment.yaml
Normal file
114
services/short/k8s/control/deployment.yaml
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: controller
|
||||
namespace: short
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: controller
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: controller
|
||||
app.kubernetes.io/instance: short
|
||||
spec:
|
||||
serviceAccountName: short-controller
|
||||
imagePullSecrets:
|
||||
- name: pull
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
runAsGroup: 65532
|
||||
fsGroup: 65532
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- topologyKey: kubernetes.io/hostname
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: controller
|
||||
app.kubernetes.io/instance: short
|
||||
containers:
|
||||
- name: controller
|
||||
image: short-image
|
||||
args:
|
||||
- controller
|
||||
env:
|
||||
- name: POD_UID
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.uid
|
||||
- name: POD_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: VAULT_ADDR
|
||||
value: https://vault.vault.svc.cluster.local:8200
|
||||
- name: OPNSENSE_URL
|
||||
value: https://10.212.111.120:8443
|
||||
- name: SSL_CERT_FILE
|
||||
value: /ca/ca.crt
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
memory: 192Mi
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
readOnly: true
|
||||
- name: ca
|
||||
mountPath: /ca
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: ca
|
||||
secret:
|
||||
secretName: ca
|
||||
- name: config
|
||||
projected:
|
||||
defaultMode: 288
|
||||
sources:
|
||||
- secret:
|
||||
name: api
|
||||
items:
|
||||
- key: private_key
|
||||
path: private_key
|
||||
- secret:
|
||||
name: cloud
|
||||
items:
|
||||
- key: config
|
||||
path: cloud.json
|
||||
- secret:
|
||||
name: opnsense
|
||||
items:
|
||||
- key: key
|
||||
path: opnsense/key
|
||||
- key: secret
|
||||
path: opnsense/secret
|
||||
- secret:
|
||||
name: ca-opnsense
|
||||
items:
|
||||
- key: ca.crt
|
||||
path: opnsense/ca.crt
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: controller
|
||||
namespace: short
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: controller
|
||||
7
services/short/k8s/control/kustomization.yaml
Normal file
7
services/short/k8s/control/kustomization.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- rbac.yaml
|
||||
- vault.yaml
|
||||
- policy.yaml
|
||||
43
services/short/k8s/control/policy.yaml
Normal file
43
services/short/k8s/control/policy.yaml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
apiVersion: cilium.io/v2
|
||||
kind: CiliumNetworkPolicy
|
||||
metadata:
|
||||
name: controller
|
||||
namespace: short
|
||||
spec:
|
||||
endpointSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: controller
|
||||
egress:
|
||||
- toEndpoints:
|
||||
- matchLabels:
|
||||
k8s:io.kubernetes.pod.namespace: kube-system
|
||||
k8s:k8s-app: kube-dns
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: '53'
|
||||
protocol: UDP
|
||||
- port: '53'
|
||||
protocol: TCP
|
||||
rules:
|
||||
dns:
|
||||
- matchPattern: '*'
|
||||
- toEndpoints:
|
||||
- matchLabels:
|
||||
k8s:io.kubernetes.pod.namespace: vault
|
||||
app.kubernetes.io/name: vault
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: '8200'
|
||||
protocol: TCP
|
||||
- toCIDR:
|
||||
- 10.212.111.120/32
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: '8443'
|
||||
protocol: TCP
|
||||
- toFQDNs:
|
||||
- matchPattern: iaas.*.oraclecloud.com
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: '443'
|
||||
protocol: TCP
|
||||
107
services/short/k8s/control/rbac.yaml
Normal file
107
services/short/k8s/control/rbac.yaml
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: short-controller
|
||||
namespace: short
|
||||
---
|
||||
apiVersion: coordination.k8s.io/v1
|
||||
kind: Lease
|
||||
metadata:
|
||||
name: controller
|
||||
namespace: short
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: controller
|
||||
namespace: short
|
||||
rules:
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resources:
|
||||
- leases
|
||||
resourceNames:
|
||||
- controller
|
||||
verbs:
|
||||
- get
|
||||
- update
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: controller
|
||||
namespace: short
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: controller
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: short-controller
|
||||
namespace: short
|
||||
---
|
||||
apiVersion: cilium.io/v2
|
||||
kind: CiliumCIDRGroup
|
||||
metadata:
|
||||
name: short
|
||||
annotations:
|
||||
kustomize.toolkit.fluxcd.io/ssa: IfNotPresent
|
||||
spec:
|
||||
externalCIDRs: []
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: short-peers
|
||||
rules:
|
||||
- apiGroups:
|
||||
- cilium.io
|
||||
resources:
|
||||
- ciliumcidrgroups
|
||||
resourceNames:
|
||||
- short
|
||||
verbs:
|
||||
- get
|
||||
- update
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: short-peers
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: short-peers
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: short-controller
|
||||
namespace: short
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: short-gateway
|
||||
namespace: kube-system
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ''
|
||||
resources:
|
||||
- services
|
||||
resourceNames:
|
||||
- cilium-gateway-gateway-short
|
||||
verbs:
|
||||
- get
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: short-gateway
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: short-gateway
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: short-controller
|
||||
namespace: short
|
||||
111
services/short/k8s/control/vault.yaml
Normal file
111
services/short/k8s/control/vault.yaml
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: api
|
||||
namespace: short
|
||||
spec:
|
||||
vaultAuthRef: short
|
||||
type: kv-v2
|
||||
mount: secret
|
||||
path: short/api
|
||||
refreshAfter: 1m
|
||||
destination:
|
||||
create: true
|
||||
overwrite: true
|
||||
name: api
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
excludes:
|
||||
- .*
|
||||
templates:
|
||||
private_key:
|
||||
text: '{{ get .Secrets "private_key" }}'
|
||||
rolloutRestartTargets:
|
||||
- kind: Deployment
|
||||
name: controller
|
||||
---
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: cloud
|
||||
namespace: short
|
||||
spec:
|
||||
vaultAuthRef: short
|
||||
type: kv-v2
|
||||
mount: secret
|
||||
path: short/cloud
|
||||
refreshAfter: 1m
|
||||
destination:
|
||||
create: true
|
||||
overwrite: true
|
||||
name: cloud
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
excludes:
|
||||
- .*
|
||||
templates:
|
||||
config:
|
||||
text: '{{ get .Secrets "config" }}'
|
||||
rolloutRestartTargets:
|
||||
- kind: Deployment
|
||||
name: controller
|
||||
---
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: opnsense
|
||||
namespace: short
|
||||
spec:
|
||||
vaultAuthRef: short
|
||||
type: kv-v2
|
||||
mount: secret
|
||||
path: opnsense/interface-watcher
|
||||
refreshAfter: 1h
|
||||
destination:
|
||||
create: true
|
||||
overwrite: true
|
||||
name: opnsense
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
excludes:
|
||||
- .*
|
||||
templates:
|
||||
key:
|
||||
text: '{{ get .Secrets "key" }}'
|
||||
secret:
|
||||
text: '{{ get .Secrets "secret" }}'
|
||||
rolloutRestartTargets:
|
||||
- kind: Deployment
|
||||
name: controller
|
||||
---
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: opnsense-ca
|
||||
namespace: short
|
||||
spec:
|
||||
vaultAuthRef: short
|
||||
type: kv-v2
|
||||
mount: secret
|
||||
path: ca/opnsense
|
||||
hmacSecretData: true
|
||||
refreshAfter: 1h
|
||||
destination:
|
||||
create: true
|
||||
overwrite: true
|
||||
name: ca-opnsense
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
excludes:
|
||||
- .*
|
||||
templates:
|
||||
ca.crt:
|
||||
text: >-
|
||||
{{- $ca := get .Secrets "certificate" -}}
|
||||
{{- if not (hasPrefix "-----BEGIN CERTIFICATE-----" $ca) -}}
|
||||
{{- fail "CA publication is pending" -}}
|
||||
{{- end -}}
|
||||
{{- $ca -}}
|
||||
rolloutRestartTargets:
|
||||
- kind: Deployment
|
||||
name: controller
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue