734 lines
24 KiB
Go
734 lines
24 KiB
Go
package oci
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
|
||
"github.com/oracle/oci-go-sdk/v65/core"
|
||
)
|
||
|
||
// VCN 是一个虚拟云网络。
|
||
type VCN struct {
|
||
ID string `json:"id"`
|
||
DisplayName string `json:"displayName"`
|
||
LifecycleState string `json:"lifecycleState"`
|
||
CompartmentID string `json:"compartmentId"`
|
||
CidrBlocks []string `json:"cidrBlocks"`
|
||
Ipv6CidrBlocks []string `json:"ipv6CidrBlocks"`
|
||
DnsLabel string `json:"dnsLabel"`
|
||
DefaultRouteTableID string `json:"defaultRouteTableId"`
|
||
DefaultSecurityListID string `json:"defaultSecurityListId"`
|
||
TimeCreated *time.Time `json:"timeCreated"`
|
||
}
|
||
|
||
// CreateVCNInput 是创建 VCN 的输入。
|
||
type CreateVCNInput struct {
|
||
Region string
|
||
CompartmentID string // 目标 compartment,空为租户根
|
||
DisplayName string
|
||
CidrBlock string
|
||
DnsLabel string
|
||
EnableIPv6 bool // 创建时同时分配 Oracle GUA IPv6 /56
|
||
}
|
||
|
||
// Subnet 是 VCN 下的一个子网。
|
||
type Subnet struct {
|
||
ID string `json:"id"`
|
||
VcnID string `json:"vcnId"`
|
||
DisplayName string `json:"displayName"`
|
||
LifecycleState string `json:"lifecycleState"`
|
||
CidrBlock string `json:"cidrBlock"`
|
||
Ipv6CidrBlock string `json:"ipv6CidrBlock"`
|
||
DnsLabel string `json:"dnsLabel"`
|
||
ProhibitPublicIP bool `json:"prohibitPublicIp"`
|
||
AvailabilityDomain string `json:"availabilityDomain"`
|
||
RouteTableID string `json:"routeTableId"`
|
||
TimeCreated *time.Time `json:"timeCreated"`
|
||
}
|
||
|
||
// CreateSubnetInput 是创建子网的输入。
|
||
type CreateSubnetInput struct {
|
||
Region string
|
||
VcnID string
|
||
DisplayName string
|
||
CidrBlock string
|
||
DnsLabel string
|
||
Ipv6CidrBlock string
|
||
ProhibitPublicIP bool
|
||
}
|
||
|
||
// SecurityRule 是安全列表中的一条规则;TCP/UDP 只支持目标端口范围。
|
||
type SecurityRule struct {
|
||
Protocol string `json:"protocol"` // all、6(TCP)、17(UDP)、1(ICMP)、58(ICMPv6)
|
||
Source string `json:"source,omitempty"`
|
||
Destination string `json:"destination,omitempty"`
|
||
IsStateless bool `json:"isStateless"`
|
||
PortMin *int `json:"portMin,omitempty"`
|
||
PortMax *int `json:"portMax,omitempty"`
|
||
IcmpType *int `json:"icmpType,omitempty"`
|
||
IcmpCode *int `json:"icmpCode,omitempty"`
|
||
Description string `json:"description,omitempty"`
|
||
}
|
||
|
||
// SecurityList 是一个安全列表及其规则。
|
||
type SecurityList struct {
|
||
ID string `json:"id"`
|
||
VcnID string `json:"vcnId"`
|
||
DisplayName string `json:"displayName"`
|
||
LifecycleState string `json:"lifecycleState"`
|
||
IngressRules []SecurityRule `json:"ingressRules"`
|
||
EgressRules []SecurityRule `json:"egressRules"`
|
||
TimeCreated *time.Time `json:"timeCreated"`
|
||
}
|
||
|
||
// CreateSecurityListInput 是创建安全列表的输入。
|
||
type CreateSecurityListInput struct {
|
||
Region string
|
||
VcnID string
|
||
DisplayName string
|
||
IngressRules []SecurityRule
|
||
EgressRules []SecurityRule
|
||
}
|
||
|
||
// UpdateSecurityListInput 是更新安全列表的输入;nil 字段保持不变,
|
||
// 规则字段一经提供即整体替换。
|
||
type UpdateSecurityListInput struct {
|
||
Region string
|
||
DisplayName string
|
||
IngressRules *[]SecurityRule
|
||
EgressRules *[]SecurityRule
|
||
}
|
||
|
||
// 自动建网使用的固定名称,便于复用而非每次新建 VCN。
|
||
const (
|
||
autoVcnName = "oci-portal-auto-vcn"
|
||
autoSubnetName = "oci-portal-auto-subnet"
|
||
autoVcnCidr = "10.0.0.0/16"
|
||
autoSubnetCidr = "10.0.0.0/24"
|
||
)
|
||
|
||
// ensureDefaultSubnet 在 subnetId 为空时准备一个可上公网的子网:
|
||
// 复用已有的 oci-portal-auto-vcn,否则新建 VCN + IGW + 默认路由 + 子网。
|
||
func (c *RealClient) ensureDefaultSubnet(ctx context.Context, cred Credentials, region string) (string, error) {
|
||
vcns, err := c.ListVCNs(ctx, cred, region)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
for _, v := range vcns {
|
||
if v.DisplayName != autoVcnName {
|
||
continue
|
||
}
|
||
// 列表项自带 VCN 所在 compartment,直接用它列子网,免去重复 GetVCN
|
||
subnets, err := listSubnets(ctx, vn, *hostCompartment(cred, v.CompartmentID), v.ID)
|
||
if err == nil && len(subnets) > 0 {
|
||
return subnets[0].ID, nil
|
||
}
|
||
}
|
||
return c.createDefaultNetwork(ctx, cred, region)
|
||
}
|
||
|
||
func (c *RealClient) createDefaultNetwork(ctx context.Context, cred Credentials, region string) (string, error) {
|
||
vcn, err := c.CreateVCN(ctx, cred, CreateVCNInput{
|
||
Region: region, DisplayName: autoVcnName, CidrBlock: autoVcnCidr, DnsLabel: "ociportalauto",
|
||
EnableIPv6: true,
|
||
})
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
// IGW 与子网都建在新 VCN 自己的 compartment,与其余关联资源保持同域
|
||
vcnCompartment := *hostCompartment(cred, vcn.CompartmentID)
|
||
igwID, err := ensureInternetGateway(ctx, vn, vcnCompartment, vcn.ID)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
if err := addInternetRoute(ctx, vn, vcn.DefaultRouteTableID, igwID, "0.0.0.0/0"); err != nil {
|
||
return "", err
|
||
}
|
||
subnet, err := c.createSubnetIn(ctx, cred, vcnCompartment, CreateSubnetInput{
|
||
Region: region, VcnID: vcn.ID, DisplayName: autoSubnetName, CidrBlock: autoSubnetCidr, DnsLabel: "ociauto",
|
||
Ipv6CidrBlock: autoSubnetIPv6Cidr(ctx, vn, vcn.ID, vcn.DefaultRouteTableID, igwID),
|
||
})
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return subnet.ID, nil
|
||
}
|
||
|
||
// autoSubnetIPv6Cidr 等待自动 VCN 的 IPv6 /56 分配就绪并补 ::/0 路由,
|
||
// 返回子网可用的 /64;IPv6 尽力而为,失败返回空串降级纯 IPv4 不阻塞建网。
|
||
func autoSubnetIPv6Cidr(ctx context.Context, vn core.VirtualNetworkClient, vcnID, routeTableID, igwID string) string {
|
||
vcnCidr, err := waitVCNIPv6Cidr(ctx, vn, vcnID)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
if err := addInternetRoute(ctx, vn, routeTableID, igwID, ipv6AnyCIDR); err != nil {
|
||
return ""
|
||
}
|
||
cidr, err := nextSubnetIPv6CIDR(vcnCidr, nil)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
return cidr
|
||
}
|
||
|
||
// addInternetRoute 在路由表补 dest → IGW,已存在则跳过。
|
||
func addInternetRoute(ctx context.Context, vn core.VirtualNetworkClient, routeTableID, igwID, dest string) error {
|
||
resp, err := vn.GetRouteTable(ctx, core.GetRouteTableRequest{RtId: &routeTableID})
|
||
if err != nil {
|
||
return fmt.Errorf("get route table: %w", err)
|
||
}
|
||
for _, rule := range resp.RouteRules {
|
||
if deref(rule.Destination) == dest {
|
||
return nil
|
||
}
|
||
}
|
||
rules := append(resp.RouteRules, core.RouteRule{
|
||
Destination: &dest,
|
||
DestinationType: core.RouteRuleDestinationTypeCidrBlock,
|
||
NetworkEntityId: &igwID,
|
||
})
|
||
_, err = vn.UpdateRouteTable(ctx, core.UpdateRouteTableRequest{
|
||
RtId: &routeTableID,
|
||
UpdateRouteTableDetails: core.UpdateRouteTableDetails{RouteRules: rules},
|
||
})
|
||
if err != nil {
|
||
return fmt.Errorf("add internet route %s: %w", dest, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c *RealClient) vcnClient(cred Credentials, region string) (core.VirtualNetworkClient, error) {
|
||
vn, err := core.NewVirtualNetworkClientWithConfigurationProvider(provider(cred))
|
||
if err != nil {
|
||
return vn, fmt.Errorf("new virtual network client: %w", err)
|
||
}
|
||
applyProxy(&vn.BaseClient, cred)
|
||
if region != "" {
|
||
vn.SetRegion(normalizeRegion(region))
|
||
}
|
||
return vn, nil
|
||
}
|
||
|
||
// ListVCNs 实现 Client。
|
||
func (c *RealClient) ListVCNs(ctx context.Context, cred Credentials, region string) ([]VCN, error) {
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var vcns []VCN
|
||
var page *string
|
||
for {
|
||
resp, err := vn.ListVcns(ctx, core.ListVcnsRequest{CompartmentId: cred.EffectiveCompartment(), Page: page})
|
||
if err != nil {
|
||
return nil, fmt.Errorf("list vcns: %w", err)
|
||
}
|
||
for _, item := range resp.Items {
|
||
vcns = append(vcns, toVCN(item))
|
||
}
|
||
if resp.OpcNextPage == nil {
|
||
return orEmpty(vcns), nil
|
||
}
|
||
page = resp.OpcNextPage
|
||
}
|
||
}
|
||
|
||
// CreateVCN 实现 Client;创建后将默认安全列表放开为出入方向全放行。
|
||
func (c *RealClient) CreateVCN(ctx context.Context, cred Credentials, in CreateVCNInput) (VCN, error) {
|
||
vn, err := c.vcnClient(cred, in.Region)
|
||
if err != nil {
|
||
return VCN{}, err
|
||
}
|
||
details := core.CreateVcnDetails{
|
||
CompartmentId: cred.EffectiveCompartment(),
|
||
CidrBlocks: []string{in.CidrBlock},
|
||
DisplayName: &in.DisplayName,
|
||
}
|
||
if in.DnsLabel != "" {
|
||
details.DnsLabel = &in.DnsLabel
|
||
}
|
||
if in.EnableIPv6 {
|
||
details.IsIpv6Enabled = &in.EnableIPv6
|
||
}
|
||
resp, err := vn.CreateVcn(ctx, core.CreateVcnRequest{CreateVcnDetails: details})
|
||
if err != nil {
|
||
return VCN{}, fmt.Errorf("create vcn: %w", err)
|
||
}
|
||
vcn := toVCN(resp.Vcn)
|
||
// VCN 初始化会异步写入默认安全列表的初始规则,未就绪时替换会被覆盖
|
||
if err := waitVCNAvailable(ctx, vn, vcn.ID); err != nil {
|
||
return vcn, err
|
||
}
|
||
if err := openDefaultSecurityList(ctx, vn, vcn.DefaultSecurityListID, in.EnableIPv6); err != nil {
|
||
return vcn, err
|
||
}
|
||
return vcn, nil
|
||
}
|
||
|
||
// waitVCNAvailable 轮询等待 VCN 进入 AVAILABLE,最长约 60 秒。
|
||
func waitVCNAvailable(ctx context.Context, vn core.VirtualNetworkClient, vcnID string) error {
|
||
for i := 0; i < 30; i++ {
|
||
resp, err := vn.GetVcn(ctx, core.GetVcnRequest{VcnId: &vcnID})
|
||
if err != nil {
|
||
return fmt.Errorf("poll vcn state: %w", err)
|
||
}
|
||
if resp.LifecycleState == core.VcnLifecycleStateAvailable {
|
||
return nil
|
||
}
|
||
select {
|
||
case <-ctx.Done():
|
||
return fmt.Errorf("poll vcn state: %w", ctx.Err())
|
||
case <-time.After(2 * time.Second):
|
||
}
|
||
}
|
||
return fmt.Errorf("poll vcn state: timed out waiting for AVAILABLE")
|
||
}
|
||
|
||
// openDefaultSecurityList 将安全列表规则替换为出入方向全放行;
|
||
// includeIPv6 时附带 ::/0 规则(OCI 拒绝在未启用 IPv6 的 VCN 上写入)。
|
||
func openDefaultSecurityList(ctx context.Context, vn core.VirtualNetworkClient, securityListID string, includeIPv6 bool) error {
|
||
protocol := "all"
|
||
v4, v6 := "0.0.0.0/0", ipv6AnyCIDR
|
||
ingress := []core.IngressSecurityRule{{Protocol: &protocol, Source: &v4}}
|
||
egress := []core.EgressSecurityRule{{Protocol: &protocol, Destination: &v4}}
|
||
if includeIPv6 {
|
||
ingress = append(ingress, core.IngressSecurityRule{Protocol: &protocol, Source: &v6})
|
||
egress = append(egress, core.EgressSecurityRule{Protocol: &protocol, Destination: &v6})
|
||
}
|
||
_, err := vn.UpdateSecurityList(ctx, core.UpdateSecurityListRequest{
|
||
SecurityListId: &securityListID,
|
||
UpdateSecurityListDetails: core.UpdateSecurityListDetails{
|
||
IngressSecurityRules: ingress,
|
||
EgressSecurityRules: egress,
|
||
},
|
||
})
|
||
if err != nil {
|
||
return fmt.Errorf("open default security list %s: %w", securityListID, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// GetVCN 实现 Client。
|
||
func (c *RealClient) GetVCN(ctx context.Context, cred Credentials, region, vcnID string) (VCN, error) {
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return VCN{}, err
|
||
}
|
||
resp, err := vn.GetVcn(ctx, core.GetVcnRequest{VcnId: &vcnID})
|
||
if err != nil {
|
||
return VCN{}, fmt.Errorf("get vcn %s: %w", vcnID, err)
|
||
}
|
||
return toVCN(resp.Vcn), nil
|
||
}
|
||
|
||
// UpdateVCN 实现 Client:当前仅支持改名。
|
||
func (c *RealClient) UpdateVCN(ctx context.Context, cred Credentials, region, vcnID, displayName string) (VCN, error) {
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return VCN{}, err
|
||
}
|
||
resp, err := vn.UpdateVcn(ctx, core.UpdateVcnRequest{
|
||
VcnId: &vcnID,
|
||
UpdateVcnDetails: core.UpdateVcnDetails{DisplayName: &displayName},
|
||
})
|
||
if err != nil {
|
||
return VCN{}, fmt.Errorf("update vcn %s: %w", vcnID, err)
|
||
}
|
||
return toVCN(resp.Vcn), nil
|
||
}
|
||
|
||
// DeleteVCN 实现 Client:级联清理子网、网关、网络安全组与非默认路由表 /
|
||
// 安全列表 / DHCP 选项后删除 VCN(见 vcndelete.go);子网被实例占用时 OCI 返回 409。
|
||
func (c *RealClient) DeleteVCN(ctx context.Context, cred Credentials, region, vcnID string) error {
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return deleteVcnCascade(ctx, vn, vcnID)
|
||
}
|
||
|
||
// vcnScopedCompartment 返回按 VCN 定位的查询与创建应使用的 compartment:
|
||
// OCI 要求 compartmentId 与资源实际所在 compartment 一致,vcnID 非空时取
|
||
// VCN 自身所在 compartment,为空(全 compartment 列表)时用生效 compartment。
|
||
func (c *RealClient) vcnScopedCompartment(ctx context.Context, cred Credentials, region, vcnID string) (string, error) {
|
||
if vcnID == "" {
|
||
return *cred.EffectiveCompartment(), nil
|
||
}
|
||
vcn, err := c.GetVCN(ctx, cred, region, vcnID)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return *hostCompartment(cred, vcn.CompartmentID), nil
|
||
}
|
||
|
||
// ListSubnets 实现 Client;vcnID 为空时列出全部子网。
|
||
func (c *RealClient) ListSubnets(ctx context.Context, cred Credentials, region, vcnID string) ([]Subnet, error) {
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
compartmentID, err := c.vcnScopedCompartment(ctx, cred, region, vcnID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return listSubnets(ctx, vn, compartmentID, vcnID)
|
||
}
|
||
|
||
func listSubnets(ctx context.Context, vn core.VirtualNetworkClient, compartmentID, vcnID string) ([]Subnet, error) {
|
||
var subnets []Subnet
|
||
var page *string
|
||
for {
|
||
req := core.ListSubnetsRequest{CompartmentId: &compartmentID, Page: page}
|
||
if vcnID != "" {
|
||
req.VcnId = &vcnID
|
||
}
|
||
resp, err := vn.ListSubnets(ctx, req)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("list subnets: %w", err)
|
||
}
|
||
for _, item := range resp.Items {
|
||
subnets = append(subnets, toSubnet(item))
|
||
}
|
||
if resp.OpcNextPage == nil {
|
||
return orEmpty(subnets), nil
|
||
}
|
||
page = resp.OpcNextPage
|
||
}
|
||
}
|
||
|
||
// CreateSubnet 实现 Client;不传 AD 时创建 regional 子网。
|
||
// 子网跟随其 VCN 所在 compartment 创建,避免跨 compartment 悬挂资源。
|
||
func (c *RealClient) CreateSubnet(ctx context.Context, cred Credentials, in CreateSubnetInput) (Subnet, error) {
|
||
compartmentID, err := c.vcnScopedCompartment(ctx, cred, in.Region, in.VcnID)
|
||
if err != nil {
|
||
return Subnet{}, err
|
||
}
|
||
return c.createSubnetIn(ctx, cred, compartmentID, in)
|
||
}
|
||
|
||
// createSubnetIn 在指定 compartment 创建子网,供已知 VCN compartment
|
||
// 的调用方(如自动建网)复用,免去一次重复的 GetVCN。
|
||
func (c *RealClient) createSubnetIn(ctx context.Context, cred Credentials, compartmentID string, in CreateSubnetInput) (Subnet, error) {
|
||
vn, err := c.vcnClient(cred, in.Region)
|
||
if err != nil {
|
||
return Subnet{}, err
|
||
}
|
||
resp, err := vn.CreateSubnet(ctx, core.CreateSubnetRequest{CreateSubnetDetails: buildSubnetDetails(compartmentID, in)})
|
||
if err != nil {
|
||
return Subnet{}, fmt.Errorf("create subnet: %w", err)
|
||
}
|
||
return toSubnet(resp.Subnet), nil
|
||
}
|
||
|
||
// buildSubnetDetails 组装创建子网请求体;可选字段仅在非零值时下发。
|
||
func buildSubnetDetails(compartmentID string, in CreateSubnetInput) core.CreateSubnetDetails {
|
||
details := core.CreateSubnetDetails{
|
||
CompartmentId: &compartmentID,
|
||
VcnId: &in.VcnID,
|
||
CidrBlock: &in.CidrBlock,
|
||
DisplayName: &in.DisplayName,
|
||
}
|
||
if in.DnsLabel != "" {
|
||
details.DnsLabel = &in.DnsLabel
|
||
}
|
||
if in.Ipv6CidrBlock != "" {
|
||
details.Ipv6CidrBlock = &in.Ipv6CidrBlock
|
||
}
|
||
if in.ProhibitPublicIP {
|
||
details.ProhibitPublicIpOnVnic = &in.ProhibitPublicIP
|
||
}
|
||
return details
|
||
}
|
||
|
||
// GetSubnet 实现 Client。
|
||
func (c *RealClient) GetSubnet(ctx context.Context, cred Credentials, region, subnetID string) (Subnet, error) {
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return Subnet{}, err
|
||
}
|
||
resp, err := vn.GetSubnet(ctx, core.GetSubnetRequest{SubnetId: &subnetID})
|
||
if err != nil {
|
||
return Subnet{}, fmt.Errorf("get subnet %s: %w", subnetID, err)
|
||
}
|
||
return toSubnet(resp.Subnet), nil
|
||
}
|
||
|
||
// UpdateSubnet 实现 Client:当前仅支持改名。
|
||
func (c *RealClient) UpdateSubnet(ctx context.Context, cred Credentials, region, subnetID, displayName string) (Subnet, error) {
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return Subnet{}, err
|
||
}
|
||
resp, err := vn.UpdateSubnet(ctx, core.UpdateSubnetRequest{
|
||
SubnetId: &subnetID,
|
||
UpdateSubnetDetails: core.UpdateSubnetDetails{DisplayName: &displayName},
|
||
})
|
||
if err != nil {
|
||
return Subnet{}, fmt.Errorf("update subnet %s: %w", subnetID, err)
|
||
}
|
||
return toSubnet(resp.Subnet), nil
|
||
}
|
||
|
||
// DeleteSubnet 实现 Client。
|
||
func (c *RealClient) DeleteSubnet(ctx context.Context, cred Credentials, region, subnetID string) error {
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if _, err := vn.DeleteSubnet(ctx, core.DeleteSubnetRequest{SubnetId: &subnetID}); err != nil {
|
||
return fmt.Errorf("delete subnet %s: %w", subnetID, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ListSecurityLists 实现 Client;vcnID 为空时列出全部安全列表。
|
||
func (c *RealClient) ListSecurityLists(ctx context.Context, cred Credentials, region, vcnID string) ([]SecurityList, error) {
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
compartmentID, err := c.vcnScopedCompartment(ctx, cred, region, vcnID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var lists []SecurityList
|
||
var page *string
|
||
for {
|
||
req := core.ListSecurityListsRequest{CompartmentId: &compartmentID, Page: page}
|
||
if vcnID != "" {
|
||
req.VcnId = &vcnID
|
||
}
|
||
resp, err := vn.ListSecurityLists(ctx, req)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("list security lists: %w", err)
|
||
}
|
||
for _, item := range resp.Items {
|
||
lists = append(lists, toSecurityList(item))
|
||
}
|
||
if resp.OpcNextPage == nil {
|
||
return orEmpty(lists), nil
|
||
}
|
||
page = resp.OpcNextPage
|
||
}
|
||
}
|
||
|
||
// CreateSecurityList 实现 Client。
|
||
// 安全列表跟随其 VCN 所在 compartment 创建,避免跨 compartment 悬挂资源。
|
||
func (c *RealClient) CreateSecurityList(ctx context.Context, cred Credentials, in CreateSecurityListInput) (SecurityList, error) {
|
||
vn, err := c.vcnClient(cred, in.Region)
|
||
if err != nil {
|
||
return SecurityList{}, err
|
||
}
|
||
compartmentID, err := c.vcnScopedCompartment(ctx, cred, in.Region, in.VcnID)
|
||
if err != nil {
|
||
return SecurityList{}, err
|
||
}
|
||
resp, err := vn.CreateSecurityList(ctx, core.CreateSecurityListRequest{
|
||
CreateSecurityListDetails: core.CreateSecurityListDetails{
|
||
CompartmentId: &compartmentID,
|
||
VcnId: &in.VcnID,
|
||
DisplayName: &in.DisplayName,
|
||
IngressSecurityRules: toIngressRules(in.IngressRules),
|
||
EgressSecurityRules: toEgressRules(in.EgressRules),
|
||
},
|
||
})
|
||
if err != nil {
|
||
return SecurityList{}, fmt.Errorf("create security list: %w", err)
|
||
}
|
||
return toSecurityList(resp.SecurityList), nil
|
||
}
|
||
|
||
// GetSecurityList 实现 Client。
|
||
func (c *RealClient) GetSecurityList(ctx context.Context, cred Credentials, region, securityListID string) (SecurityList, error) {
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return SecurityList{}, err
|
||
}
|
||
resp, err := vn.GetSecurityList(ctx, core.GetSecurityListRequest{SecurityListId: &securityListID})
|
||
if err != nil {
|
||
return SecurityList{}, fmt.Errorf("get security list %s: %w", securityListID, err)
|
||
}
|
||
return toSecurityList(resp.SecurityList), nil
|
||
}
|
||
|
||
// UpdateSecurityList 实现 Client。
|
||
func (c *RealClient) UpdateSecurityList(ctx context.Context, cred Credentials, securityListID string, in UpdateSecurityListInput) (SecurityList, error) {
|
||
vn, err := c.vcnClient(cred, in.Region)
|
||
if err != nil {
|
||
return SecurityList{}, err
|
||
}
|
||
details := core.UpdateSecurityListDetails{}
|
||
if in.DisplayName != "" {
|
||
details.DisplayName = &in.DisplayName
|
||
}
|
||
if in.IngressRules != nil {
|
||
details.IngressSecurityRules = toIngressRules(*in.IngressRules)
|
||
}
|
||
if in.EgressRules != nil {
|
||
details.EgressSecurityRules = toEgressRules(*in.EgressRules)
|
||
}
|
||
resp, err := vn.UpdateSecurityList(ctx, core.UpdateSecurityListRequest{
|
||
SecurityListId: &securityListID,
|
||
UpdateSecurityListDetails: details,
|
||
})
|
||
if err != nil {
|
||
return SecurityList{}, fmt.Errorf("update security list %s: %w", securityListID, err)
|
||
}
|
||
return toSecurityList(resp.SecurityList), nil
|
||
}
|
||
|
||
// DeleteSecurityList 实现 Client。
|
||
func (c *RealClient) DeleteSecurityList(ctx context.Context, cred Credentials, region, securityListID string) error {
|
||
vn, err := c.vcnClient(cred, region)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if _, err := vn.DeleteSecurityList(ctx, core.DeleteSecurityListRequest{SecurityListId: &securityListID}); err != nil {
|
||
return fmt.Errorf("delete security list %s: %w", securityListID, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func toVCN(v core.Vcn) VCN {
|
||
return VCN{
|
||
ID: deref(v.Id),
|
||
DisplayName: deref(v.DisplayName),
|
||
LifecycleState: string(v.LifecycleState),
|
||
CompartmentID: deref(v.CompartmentId),
|
||
CidrBlocks: orEmpty(v.CidrBlocks),
|
||
Ipv6CidrBlocks: orEmpty(v.Ipv6CidrBlocks),
|
||
DnsLabel: deref(v.DnsLabel),
|
||
DefaultRouteTableID: deref(v.DefaultRouteTableId),
|
||
DefaultSecurityListID: deref(v.DefaultSecurityListId),
|
||
TimeCreated: sdkTime(v.TimeCreated),
|
||
}
|
||
}
|
||
|
||
func toSubnet(s core.Subnet) Subnet {
|
||
return Subnet{
|
||
ID: deref(s.Id),
|
||
VcnID: deref(s.VcnId),
|
||
DisplayName: deref(s.DisplayName),
|
||
LifecycleState: string(s.LifecycleState),
|
||
CidrBlock: deref(s.CidrBlock),
|
||
Ipv6CidrBlock: deref(s.Ipv6CidrBlock),
|
||
DnsLabel: deref(s.DnsLabel),
|
||
ProhibitPublicIP: s.ProhibitPublicIpOnVnic != nil && *s.ProhibitPublicIpOnVnic,
|
||
AvailabilityDomain: deref(s.AvailabilityDomain),
|
||
RouteTableID: deref(s.RouteTableId),
|
||
TimeCreated: sdkTime(s.TimeCreated),
|
||
}
|
||
}
|
||
|
||
func toSecurityList(sl core.SecurityList) SecurityList {
|
||
out := SecurityList{
|
||
ID: deref(sl.Id),
|
||
VcnID: deref(sl.VcnId),
|
||
DisplayName: deref(sl.DisplayName),
|
||
LifecycleState: string(sl.LifecycleState),
|
||
TimeCreated: sdkTime(sl.TimeCreated),
|
||
IngressRules: make([]SecurityRule, 0, len(sl.IngressSecurityRules)),
|
||
EgressRules: make([]SecurityRule, 0, len(sl.EgressSecurityRules)),
|
||
}
|
||
for _, r := range sl.IngressSecurityRules {
|
||
rule := SecurityRule{
|
||
Protocol: deref(r.Protocol),
|
||
Source: deref(r.Source),
|
||
IsStateless: r.IsStateless != nil && *r.IsStateless,
|
||
Description: deref(r.Description),
|
||
}
|
||
fillRuleOptions(&rule, r.TcpOptions, r.UdpOptions, r.IcmpOptions)
|
||
out.IngressRules = append(out.IngressRules, rule)
|
||
}
|
||
for _, r := range sl.EgressSecurityRules {
|
||
rule := SecurityRule{
|
||
Protocol: deref(r.Protocol),
|
||
Destination: deref(r.Destination),
|
||
IsStateless: r.IsStateless != nil && *r.IsStateless,
|
||
Description: deref(r.Description),
|
||
}
|
||
fillRuleOptions(&rule, r.TcpOptions, r.UdpOptions, r.IcmpOptions)
|
||
out.EgressRules = append(out.EgressRules, rule)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func fillRuleOptions(rule *SecurityRule, tcp *core.TcpOptions, udp *core.UdpOptions, icmp *core.IcmpOptions) {
|
||
switch {
|
||
case tcp != nil && tcp.DestinationPortRange != nil:
|
||
rule.PortMin = tcp.DestinationPortRange.Min
|
||
rule.PortMax = tcp.DestinationPortRange.Max
|
||
case udp != nil && udp.DestinationPortRange != nil:
|
||
rule.PortMin = udp.DestinationPortRange.Min
|
||
rule.PortMax = udp.DestinationPortRange.Max
|
||
case icmp != nil:
|
||
rule.IcmpType = icmp.Type
|
||
rule.IcmpCode = icmp.Code
|
||
}
|
||
}
|
||
|
||
func toIngressRules(rules []SecurityRule) []core.IngressSecurityRule {
|
||
out := make([]core.IngressSecurityRule, 0, len(rules))
|
||
for i := range rules {
|
||
r := rules[i]
|
||
sdk := core.IngressSecurityRule{
|
||
Protocol: &r.Protocol,
|
||
Source: &r.Source,
|
||
IsStateless: &rules[i].IsStateless,
|
||
}
|
||
if r.Description != "" {
|
||
sdk.Description = &rules[i].Description
|
||
}
|
||
sdk.TcpOptions, sdk.UdpOptions, sdk.IcmpOptions = buildRuleOptions(r)
|
||
out = append(out, sdk)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func toEgressRules(rules []SecurityRule) []core.EgressSecurityRule {
|
||
out := make([]core.EgressSecurityRule, 0, len(rules))
|
||
for i := range rules {
|
||
r := rules[i]
|
||
sdk := core.EgressSecurityRule{
|
||
Protocol: &r.Protocol,
|
||
Destination: &r.Destination,
|
||
IsStateless: &rules[i].IsStateless,
|
||
}
|
||
if r.Description != "" {
|
||
sdk.Description = &rules[i].Description
|
||
}
|
||
sdk.TcpOptions, sdk.UdpOptions, sdk.IcmpOptions = buildRuleOptions(r)
|
||
out = append(out, sdk)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// buildRuleOptions 按协议号构造端口或 ICMP 选项:6=TCP、17=UDP、1/58=ICMP(v6)。
|
||
func buildRuleOptions(r SecurityRule) (*core.TcpOptions, *core.UdpOptions, *core.IcmpOptions) {
|
||
if r.PortMin != nil {
|
||
portMax := r.PortMax
|
||
if portMax == nil {
|
||
portMax = r.PortMin
|
||
}
|
||
pr := &core.PortRange{Min: r.PortMin, Max: portMax}
|
||
switch r.Protocol {
|
||
case "6":
|
||
return &core.TcpOptions{DestinationPortRange: pr}, nil, nil
|
||
case "17":
|
||
return nil, &core.UdpOptions{DestinationPortRange: pr}, nil
|
||
}
|
||
}
|
||
if r.IcmpType != nil && (r.Protocol == "1" || r.Protocol == "58") {
|
||
return nil, nil, &core.IcmpOptions{Type: r.IcmpType, Code: r.IcmpCode}
|
||
}
|
||
return nil, nil, nil
|
||
}
|