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-。 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 }