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

175 lines
6.0 KiB
Go

package oci
import (
"context"
"fmt"
"os"
"strings"
"testing"
"time"
)
// TestRealOCIInstanceLifecycle 走真实实例完整生命周期:
// 建网络 → 查 AD/镜像 → 创建 A1 实例 → 等待 RUNNING → 校验 IP 补全 →
// 改名 → 引导卷查改 → STOP → 终止(不保留卷)→ 清理网络。
// A1 容量不足时跳过(试用区域常见)。
func TestRealOCIInstanceLifecycle(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(), 15*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.98.0.0/16", DnsLabel: "e2einst",
})
if err != nil {
t.Fatalf("CreateVCN: %v", err)
}
defer cleanupVCN(t, client, cred, vcn.ID)
subnet, err := client.CreateSubnet(ctx, cred, CreateSubnetInput{
VcnID: vcn.ID, DisplayName: prefix + "-subnet", CidrBlock: "10.98.1.0/24", DnsLabel: "e2esub",
})
if err != nil {
t.Fatalf("CreateSubnet: %v", err)
}
ads, err := client.ListAvailabilityDomains(ctx, cred, "")
if err != nil || len(ads) == 0 {
t.Fatalf("ListAvailabilityDomains: %v (got %d)", err, len(ads))
}
images, err := client.ListImages(ctx, cred, ImagesQuery{
OperatingSystem: "Canonical Ubuntu", Shape: "VM.Standard.A1.Flex",
})
if err != nil || len(images) == 0 {
t.Fatalf("ListImages: %v (got %d)", err, len(images))
}
t.Logf("using ad=%s image=%s", ads[0], images[0].DisplayName)
instance, err := client.LaunchInstance(ctx, cred, CreateInstanceInput{
AvailabilityDomain: ads[0],
DisplayName: prefix + "-vm",
Shape: "VM.Standard.A1.Flex",
Ocpus: 1,
MemoryInGBs: 6,
ImageID: images[0].ID,
BootVolumeSizeGBs: 50,
SubnetID: subnet.ID,
AssignPublicIP: true,
SSHPublicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEfake+e2e+key oci-portal-e2e",
})
if err != nil {
if strings.Contains(err.Error(), "Out of host capacity") || strings.Contains(err.Error(), "OutOfCapacity") {
t.Skipf("A1 capacity unavailable in this region: %v", err)
}
t.Fatalf("LaunchInstance: %v", err)
}
t.Logf("launched instance %s (%s)", instance.DisplayName, instance.ID)
defer terminateAndWait(t, client, cred, instance.ID)
if err := waitInstanceState(ctx, client, cred, instance.ID, "RUNNING"); err != nil {
t.Fatalf("wait RUNNING: %v", err)
}
got, err := client.GetInstance(ctx, cred, "", instance.ID)
if err != nil {
t.Fatalf("GetInstance: %v", err)
}
t.Logf("instance running: shape=%s %gocpu/%gGB privateIp=%s publicIp=%s",
got.Shape, got.Ocpus, got.MemoryInGBs, got.PrivateIP, got.PublicIP)
if got.PrivateIP == "" {
t.Error("PrivateIP empty, want enriched from VNIC")
}
if got.PublicIP == "" {
t.Error("PublicIP empty, want assigned")
}
if got.SubnetID != subnet.ID {
t.Errorf("SubnetID = %s, want %s", got.SubnetID, subnet.ID)
}
if _, err := client.UpdateInstance(ctx, cred, instance.ID, UpdateInstanceInput{DisplayName: prefix + "-vm-renamed"}); err != nil {
t.Errorf("UpdateInstance: %v", err)
}
verifyBootVolume(ctx, t, client, cred, ads[0], instance.ID, prefix)
if _, err := client.InstanceAction(ctx, cred, "", instance.ID, "STOP"); err != nil {
t.Fatalf("InstanceAction STOP: %v", err)
}
if err := waitInstanceState(ctx, client, cred, instance.ID, "STOPPED"); err != nil {
t.Fatalf("wait STOPPED: %v", err)
}
t.Log("instance stopped, terminating")
}
func verifyBootVolume(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, ad, instanceID, prefix string) {
t.Helper()
volumes, err := client.ListBootVolumes(ctx, cred, "", ad)
if err != nil {
t.Errorf("ListBootVolumes: %v", err)
return
}
for _, v := range volumes {
detail, err := client.GetBootVolume(ctx, cred, "", v.ID)
if err != nil {
t.Errorf("GetBootVolume: %v", err)
continue
}
if detail.AttachedInstanceID != instanceID {
continue
}
t.Logf("boot volume %s size=%dGB vpus=%d attached=%s",
detail.DisplayName, detail.SizeInGBs, detail.VpusPerGB, detail.AttachedInstanceID)
if detail.SizeInGBs != 50 {
t.Errorf("boot volume size = %d, want 50", detail.SizeInGBs)
}
if _, err := client.UpdateBootVolume(ctx, cred, v.ID, UpdateBootVolumeInput{DisplayName: prefix + "-bv"}); err != nil {
t.Errorf("UpdateBootVolume: %v", err)
}
return
}
t.Errorf("no boot volume attached to instance %s found in %s", instanceID, ad)
}
// waitInstanceState 轮询实例直到到达目标状态。
func waitInstanceState(ctx context.Context, client *RealClient, cred Credentials, instanceID, want string) error {
for i := 0; i < 60; i++ {
inst, err := client.GetInstance(ctx, cred, "", instanceID)
if err != nil {
return fmt.Errorf("poll instance: %w", err)
}
if inst.LifecycleState == want {
return nil
}
select {
case <-ctx.Done():
return fmt.Errorf("poll instance state %s: %w", want, ctx.Err())
case <-time.After(5 * time.Second):
}
}
return fmt.Errorf("poll instance state %s: timed out", want)
}
// terminateAndWait 终止实例(不保留引导卷)并等待 TERMINATED,
// 保证后续网络清理不被占用的 VNIC 阻塞。
func terminateAndWait(t *testing.T, client *RealClient, cred Credentials, instanceID string) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute)
defer cancel()
inst, err := client.GetInstance(ctx, cred, "", instanceID)
if err == nil && inst.LifecycleState == "TERMINATED" {
return
}
if err := client.TerminateInstance(ctx, cred, "", instanceID, false); err != nil {
t.Errorf("cleanup: terminate instance: %v", err)
return
}
if err := waitInstanceState(ctx, client, cred, instanceID, "TERMINATED"); err != nil {
t.Errorf("cleanup: wait TERMINATED: %v (manual cleanup may be required)", err)
return
}
t.Logf("cleanup: instance %s terminated", instanceID)
}