232 lines
7.6 KiB
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 }
|