package deploy import ( "context" "fmt" "io" "net/http" "strings" "time" ) const ec2IMDSBase = "http://169.254.169.254/latest" type CloudInstanceMeta struct { VpcID string `json:"vpc_id,omitempty"` SubnetID string `json:"subnet_id,omitempty"` Region string `json:"region,omitempty"` } func (m CloudInstanceMeta) Present() bool { return strings.TrimSpace(m.VpcID) != "" || strings.TrimSpace(m.SubnetID) != "" || strings.TrimSpace(m.Region) != "" } var ec2IMDSReadMeta = readEC2InstanceMetaImpl func ReadEC2InstanceMeta() CloudInstanceMeta { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() meta, _ := ec2IMDSReadMeta(ctx) return meta } func readEC2InstanceMetaImpl(ctx context.Context) (CloudInstanceMeta, error) { token, err := fetchEC2IMDSToken(ctx) if err != nil { return CloudInstanceMeta{}, err } region, _ := fetchEC2IMDSPath(ctx, token, "meta-data/placement/region") if strings.TrimSpace(region) == "" { az, _ := fetchEC2IMDSPath(ctx, token, "meta-data/placement/availability-zone") az = strings.TrimSpace(az) if len(az) > 1 { region = az[:len(az)-1] } } macs, err := fetchEC2IMDSPath(ctx, token, "meta-data/network/interfaces/macs/") if err != nil || strings.TrimSpace(macs) == "" { return CloudInstanceMeta{Region: strings.TrimSpace(region)}, nil } mac := strings.TrimSpace(strings.Split(macs, "\n")[0]) if mac == "" { return CloudInstanceMeta{Region: strings.TrimSpace(region)}, nil } if !strings.HasSuffix(mac, "/") { mac += "/" } base := "meta-data/network/interfaces/macs/" + mac vpcID, _ := fetchEC2IMDSPath(ctx, token, base+"vpc-id") subnetID, _ := fetchEC2IMDSPath(ctx, token, base+"subnet-id") return CloudInstanceMeta{VpcID: strings.TrimSpace(vpcID), SubnetID: strings.TrimSpace(subnetID), Region: strings.TrimSpace(region)}, nil } func fetchEC2IMDSToken(ctx context.Context) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodPut, ec2IMDSBase+"/api/token", nil) if err != nil { return "", err } req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "60") resp, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("imds token HTTP %d", resp.StatusCode) } body, err := io.ReadAll(io.LimitReader(resp.Body, 128)) return strings.TrimSpace(string(body)), err } func fetchEC2IMDSPath(ctx context.Context, token, path string) (string, error) { path = strings.TrimPrefix(path, "/") req, err := http.NewRequestWithContext(ctx, http.MethodGet, ec2IMDSBase+"/"+path, nil) if err != nil { return "", err } if token != "" { req.Header.Set("X-aws-ec2-metadata-token", token) } resp, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("imds HTTP %d", resp.StatusCode) } body, err := io.ReadAll(io.LimitReader(resp.Body, 64<<10)) return strings.TrimSpace(string(body)), err } // ec2IMDSToken and ec2IMDSFetch are shared helpers for cloud venue probing. func ec2IMDSToken(ctx context.Context) (string, error) { return fetchEC2IMDSToken(ctx) } func ec2IMDSFetch(ctx context.Context, token, path string) (string, error) { return fetchEC2IMDSPath(ctx, token, path) }