Files
oci-portal/internal/oci/real_network_integration_test.go

232 lines
7.6 KiB
Go

package oci
import (
"context"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/oracle/oci-go-sdk/v65/core"
)
// TestRealOCIImages 用试用期 key 验证镜像列表查询(只读)。
func TestRealOCIImages(t *testing.T) {
if os.Getenv("OCI_INTEGRATION_TEST") != "1" {
t.Skip("set OCI_INTEGRATION_TEST=1 to run real OCI tests")
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
cred := loadTestCredentials(t, "试用期")
client := NewClient()
images, err := client.ListImages(ctx, cred, ImagesQuery{
OperatingSystem: "Canonical Ubuntu",
Shape: "VM.Standard.A1.Flex",
})
if err != nil {
t.Fatalf("ListImages: %v", err)
}
if len(images) == 0 {
t.Fatal("ListImages returned empty list for Ubuntu on A1")
}
for _, img := range images[:min(3, len(images))] {
t.Logf("image %s os=%s %s size=%dMB", img.DisplayName, img.OperatingSystem, img.OperatingSystemVersion, img.SizeInMBs)
if img.ID == "" || img.OperatingSystem == "" {
t.Errorf("image %+v has empty mandatory field", img)
}
}
}
// TestRealOCINetworkCRUDAndIPv6 走全链路:建 VCN → 建子网 → 建安全列表 →
// 一键 IPv6 → 校验,结束后尽力清理全部资源。
// 会创建真实云资源,资源名前缀 oci-portal-e2e-<timestamp>。
func TestRealOCINetworkCRUDAndIPv6(t *testing.T) {
if os.Getenv("OCI_INTEGRATION_TEST") != "1" {
t.Skip("set OCI_INTEGRATION_TEST=1 to run real OCI tests")
}
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Minute)
defer cancel()
cred := loadTestCredentials(t, "试用期")
client := NewClient()
prefix := fmt.Sprintf("oci-portal-e2e-%d", time.Now().Unix())
vcn, err := client.CreateVCN(ctx, cred, CreateVCNInput{
DisplayName: prefix + "-vcn",
CidrBlock: "10.99.0.0/16",
DnsLabel: "ociportale2e",
})
if err != nil {
t.Fatalf("CreateVCN: %v", err)
}
t.Logf("created vcn %s (%s)", vcn.DisplayName, vcn.ID)
defer cleanupVCN(t, client, cred, vcn.ID)
if _, err := client.UpdateVCN(ctx, cred, "", vcn.ID, prefix+"-vcn-renamed"); err != nil {
t.Errorf("UpdateVCN: %v", err)
}
subnet, err := client.CreateSubnet(ctx, cred, CreateSubnetInput{
VcnID: vcn.ID,
DisplayName: prefix + "-subnet",
CidrBlock: "10.99.1.0/24",
DnsLabel: "e2esub",
})
if err != nil {
t.Fatalf("CreateSubnet: %v", err)
}
t.Logf("created subnet %s (%s)", subnet.DisplayName, subnet.ID)
secList, err := client.CreateSecurityList(ctx, cred, CreateSecurityListInput{
VcnID: vcn.ID,
DisplayName: prefix + "-seclist",
IngressRules: []SecurityRule{
{Protocol: "6", Source: "0.0.0.0/0", PortMin: intPtr(22), PortMax: intPtr(22), Description: "ssh"},
},
EgressRules: []SecurityRule{{Protocol: "all", Destination: "0.0.0.0/0"}},
})
if err != nil {
t.Fatalf("CreateSecurityList: %v", err)
}
t.Logf("created security list %s (%s)", secList.DisplayName, secList.ID)
if len(secList.IngressRules) != 1 || secList.IngressRules[0].PortMin == nil || *secList.IngressRules[0].PortMin != 22 {
t.Errorf("ingress rules = %+v, want one ssh rule with port 22", secList.IngressRules)
}
updated, err := client.UpdateSecurityList(ctx, cred, secList.ID, UpdateSecurityListInput{
IngressRules: &[]SecurityRule{
{Protocol: "6", Source: "0.0.0.0/0", PortMin: intPtr(22), PortMax: intPtr(22), Description: "ssh"},
{Protocol: "6", Source: "0.0.0.0/0", PortMin: intPtr(443), PortMax: intPtr(443), Description: "https"},
},
})
if err != nil {
t.Fatalf("UpdateSecurityList: %v", err)
}
if len(updated.IngressRules) != 2 {
t.Errorf("ingress rules after update = %d, want 2", len(updated.IngressRules))
}
steps, err := client.EnableVCNIPv6(ctx, cred, "", vcn.ID)
if err != nil {
t.Fatalf("EnableVCNIPv6: %v", err)
}
for _, s := range steps {
t.Logf("ipv6 step=%s status=%s %s", s.Step, s.Status, s.Detail)
if s.Status == "failed" {
t.Errorf("ipv6 step %s failed: %s", s.Step, s.Detail)
}
}
vcnAfter, err := client.GetVCN(ctx, cred, "", vcn.ID)
if err != nil {
t.Fatalf("GetVCN after ipv6: %v", err)
}
if len(vcnAfter.Ipv6CidrBlocks) == 0 {
t.Error("vcn has no ipv6 cidr after enable-ipv6")
}
subnetAfter, err := client.GetSubnet(ctx, cred, "", subnet.ID)
if err != nil {
t.Fatalf("GetSubnet after ipv6: %v", err)
}
if subnetAfter.Ipv6CidrBlock == "" {
t.Error("subnet has no ipv6 cidr after enable-ipv6")
}
t.Logf("vcn ipv6=%v subnet ipv6=%s", vcnAfter.Ipv6CidrBlocks, subnetAfter.Ipv6CidrBlock)
// 幂等:重复执行应全部 skipped
steps2, err := client.EnableVCNIPv6(ctx, cred, "", vcn.ID)
if err != nil {
t.Fatalf("EnableVCNIPv6 twice: %v", err)
}
for _, s := range steps2 {
if s.Status != "skipped" {
t.Errorf("rerun step %s status = %s, want skipped", s.Step, s.Status)
}
}
if err := client.DeleteSecurityList(ctx, cred, "", secList.ID); err != nil {
t.Errorf("DeleteSecurityList: %v", err)
}
if err := client.DeleteSubnet(ctx, cred, "", subnet.ID); err != nil {
t.Errorf("DeleteSubnet: %v", err)
}
}
// cleanupVCN 尽力清理 VCN 及其关联资源:清空默认路由表、删 IGW、
// 删残留子网与自建安全列表,最后删 VCN。
func cleanupVCN(t *testing.T, client *RealClient, cred Credentials, vcnID string) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
vn, err := client.vcnClient(cred, "")
if err != nil {
t.Errorf("cleanup: vcn client: %v", err)
return
}
vcnResp, err := vn.GetVcn(ctx, core.GetVcnRequest{VcnId: &vcnID})
if err != nil {
t.Errorf("cleanup: get vcn: %v", err)
return
}
if _, err := vn.UpdateRouteTable(ctx, core.UpdateRouteTableRequest{
RtId: vcnResp.DefaultRouteTableId,
UpdateRouteTableDetails: core.UpdateRouteTableDetails{RouteRules: []core.RouteRule{}},
}); err != nil {
t.Logf("cleanup: clear route table: %v", err)
}
igws, err := vn.ListInternetGateways(ctx, core.ListInternetGatewaysRequest{
CompartmentId: &cred.TenancyOCID, VcnId: &vcnID,
})
if err == nil {
for _, igw := range igws.Items {
if _, err := vn.DeleteInternetGateway(ctx, core.DeleteInternetGatewayRequest{IgId: igw.Id}); err != nil {
t.Logf("cleanup: delete igw: %v", err)
}
}
}
if subnets, err := listSubnets(ctx, vn, cred.TenancyOCID, vcnID); err == nil {
for _, s := range subnets {
if err := deleteSubnetWithRetry(ctx, client, cred, s.ID); err != nil {
t.Logf("cleanup: delete subnet %s: %v", s.DisplayName, err)
}
}
}
if lists, err := client.ListSecurityLists(ctx, cred, "", vcnID); err == nil {
for _, sl := range lists {
// 默认安全列表的名字包含 VCN 名,须用前缀匹配排除,它随 VCN 一起删除
if strings.HasPrefix(sl.DisplayName, "oci-portal-e2e-") {
if err := client.DeleteSecurityList(ctx, cred, "", sl.ID); err != nil {
t.Logf("cleanup: delete security list %s: %v", sl.DisplayName, err)
}
}
}
}
if err := client.DeleteVCN(ctx, cred, "", vcnID); err != nil {
t.Errorf("cleanup: delete vcn: %v (manual cleanup may be required)", err)
return
}
t.Logf("cleanup: vcn %s deleted", vcnID)
}
// deleteSubnetWithRetry 删除子网;实例终止后 VNIC 异步释放,子网短暂
// 保持被引用状态,409 Conflict 时等待重试,最长约 60 秒。
func deleteSubnetWithRetry(ctx context.Context, client *RealClient, cred Credentials, subnetID string) error {
var err error
for i := 0; i < 12; i++ {
if err = client.DeleteSubnet(ctx, cred, "", subnetID); err == nil {
return nil
}
if !strings.Contains(err.Error(), "Conflict") {
return err
}
select {
case <-ctx.Done():
return err
case <-time.After(5 * time.Second):
}
}
return err
}
func intPtr(n int) *int { return &n }