diff --git a/api/holodeck/v1alpha1/types.go b/api/holodeck/v1alpha1/types.go index 213dc1430..081e0928d 100644 --- a/api/holodeck/v1alpha1/types.go +++ b/api/holodeck/v1alpha1/types.go @@ -65,7 +65,7 @@ type Instance struct { Region string `json:"region"` // +optional - IngresIpRanges []string `json:"ingressIpRanges"` + IngressIpRanges []string `json:"ingressIpRanges"` // +optional HostUrl string `json:"hostUrl"` } diff --git a/api/holodeck/v1alpha1/zz_generated.deepcopy.go b/api/holodeck/v1alpha1/zz_generated.deepcopy.go index b387e7491..d84e431f2 100644 --- a/api/holodeck/v1alpha1/zz_generated.deepcopy.go +++ b/api/holodeck/v1alpha1/zz_generated.deepcopy.go @@ -227,8 +227,8 @@ func (in *Image) DeepCopy() *Image { func (in *Instance) DeepCopyInto(out *Instance) { *out = *in in.Image.DeepCopyInto(&out.Image) - if in.IngresIpRanges != nil { - in, out := &in.IngresIpRanges, &out.IngresIpRanges + if in.IngressIpRanges != nil { + in, out := &in.IngressIpRanges, &out.IngressIpRanges *out = make([]string, len(*in)) copy(*out, *in) } diff --git a/docs/README.md b/docs/README.md index 8fc423680..ab12fe94e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,8 @@ get started, use, and contribute to Holodeck. including coding standards and PR process. - [Examples](examples/README.md): Example configuration files and usage scenarios. - [Guides](guides/README.md): In-depth guides and tutorials for advanced usage. +- [IP Detection Guide](guides/ip-detection.md): Learn about automatic IP + detection for AWS environments. - [Latest Release](https://github.com/NVIDIA/holodeck/releases/latest) --- diff --git a/docs/commands/create.md b/docs/commands/create.md index 5e03433ac..9f06ef323 100644 --- a/docs/commands/create.md +++ b/docs/commands/create.md @@ -56,6 +56,56 @@ spec: version: v1.28.5 ``` +## Automated IP Detection + +Holodeck now automatically detects your public IP address when creating AWS +environments. This eliminates the need to manually specify your IP address in +the configuration file. + +### How It Works + +- **Automatic Detection**: Your public IP is automatically detected using + reliable HTTP services +- **Fallback Services**: Multiple IP detection services ensure reliability + (ipify.org, ifconfig.me, icanhazip.com, ident.me) +- **Proper CIDR Format**: IP addresses are automatically formatted with + `/32` suffix for AWS compatibility +- **Timeout Protection**: 15-second overall timeout with 5-second + per-service timeout + +### Configuration + +The `ingressIpRanges` field in your configuration is now optional for AWS environments: + +```yaml +spec: + provider: aws + instance: + type: g4dn.xlarge + region: us-west-2 + # ingressIpRanges is now optional - your IP is detected automatically + # ingressIpRanges: + # - "192.168.1.1/32" # Only needed for additional IP ranges +``` + +### Manual IP Override + +If you need to specify additional IP ranges or override the automatic +detection, you can still use the `ingressIpRanges` field: + +```yaml +spec: + provider: aws + instance: + type: g4dn.xlarge + region: us-west-2 + ingressIpRanges: + - "10.0.0.0/8" # Corporate network + - "172.16.0.0/12" # Additional network +``` + +Your detected public IP will be automatically added to the security group rules. + ## Sample Output ```text @@ -68,6 +118,8 @@ Created instance 123e4567-e89b-12d3-a456-426614174000 invalid. - `failed to provision: ...` — Provisioning failed due to a configuration or provider error. +- `error getting IP address: ...` — IP detection failed (check network + connectivity to IP detection services). - `Created instance ` — Success log after creation. ## Supported NVIDIA Driver Versions diff --git a/docs/examples/README.md b/docs/examples/README.md index baa832129..ad28dfafb 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -51,6 +51,44 @@ A sample kind cluster configuration for use with the kind installer. --- +## Updated AWS Examples + +The example configurations now show that `ingressIpRanges` is optional: + +**File:** [`examples/aws_kubeadm.yaml`](../../examples/aws_kubeadm.yaml) + +```yaml +spec: + provider: aws + instance: + type: g4dn.xlarge + region: us-west-2 + # ingressIpRanges is now optional - your IP is detected automatically + image: + architecture: amd64 +``` + +**File:** [`examples/aws_kind.yaml`](../../examples/aws_kind.yaml) + +```yaml +spec: + provider: aws + instance: + type: g4dn.xlarge + region: eu-north-1 + # ingressIpRanges is now optional - your IP is detected automatically + image: + architecture: amd64 +``` + +### Benefits of Automated IP Detection + +- **Simplified Configuration**: No need to manually find and specify your + public IP +- **Dynamic IP Support**: Works with changing IP addresses (DHCP, mobile networks) +- **Reduced Errors**: Eliminates "CIDR block malformed" errors +- **Better Security**: Ensures only your current public IP has access + ## How to Use These Examples 1. Copy the desired YAML file to your working directory (optional). diff --git a/docs/guides/README.md b/docs/guides/README.md index 380d2ca31..da75f5f58 100644 --- a/docs/guides/README.md +++ b/docs/guides/README.md @@ -2,9 +2,12 @@ This section is for in-depth guides and tutorials related to Holodeck. -- If you are looking for step-by-step instructions or advanced usage, guides - will be listed here as they are added. -- To contribute a guide, simply add a new Markdown file to this folder and - update this README with a link. +## Available Guides -*No guides are available yet. Stay tuned!* +- [IP Detection Guide](ip-detection.md): Learn about automatic IP detection for + AWS environments, including configuration, troubleshooting, and best practices. + +## Contributing + +To contribute a guide, simply add a new Markdown file to this folder and +update this README with a link. diff --git a/docs/guides/ip-detection.md b/docs/guides/ip-detection.md new file mode 100644 index 000000000..244f268a0 --- /dev/null +++ b/docs/guides/ip-detection.md @@ -0,0 +1,126 @@ +# IP Detection Guide + +## Overview + +Holodeck automatically detects your public IP address when creating AWS +environments, eliminating the need to manually configure security group rules. + +## How It Works + +### Detection Process + +1. **Service Priority**: Tries multiple IP detection services in order +1. **Fallback Strategy**: If one service fails, automatically tries the next +1. **Validation**: Ensures detected IP is a valid public IPv4 address +1. **CIDR Formatting**: Automatically adds `/32` suffix for AWS compatibility + +### Supported Services + +- `https://api.ipify.org?format=text` (Primary) +- `https://ifconfig.me/ip` (Fallback 1) +- `https://icanhazip.com` (Fallback 2) +- `https://ident.me` (Fallback 3) + +### Timeout Configuration + +- **Overall Timeout**: 15 seconds +- **Per-Service Timeout**: 5 seconds +- **Context Support**: Proper cancellation and timeout handling + +## Configuration Examples + +### Basic Usage (Recommended) + +```yaml +apiVersion: holodeck.nvidia.com/v1alpha1 +kind: Environment +metadata: + name: my-environment +spec: + provider: aws + instance: + type: g4dn.xlarge + region: us-west-2 + # No ingressIpRanges needed - IP detected automatically +``` + +### With Additional IP Ranges + +```yaml +apiVersion: holodeck.nvidia.com/v1alpha1 +kind: Environment +metadata: + name: my-environment +spec: + provider: aws + instance: + type: g4dn.xlarge + region: us-west-2 + ingressIpRanges: + - "10.0.0.0/8" # Corporate network + - "172.16.0.0/12" # Additional network + # Your detected IP will be automatically added +``` + +## Troubleshooting + +### Common Issues + +1. **Network Connectivity**: Ensure outbound internet access to IP detection services +1. **Firewall Rules**: Corporate firewalls may block IP detection services +1. **Proxy Configuration**: Proxy settings may affect IP detection + +### Manual Override + +If automatic detection fails, you can manually specify your IP: + +```yaml +spec: + provider: aws + instance: + type: g4dn.xlarge + region: us-west-2 + ingressIpRanges: + - "YOUR_PUBLIC_IP/32" # Replace with your actual public IP +``` + +### Debugging + +To debug IP detection issues: + +```bash +# Test IP detection manually +curl https://api.ipify.org?format=text +curl https://ifconfig.me/ip +curl https://icanhazip.com +curl https://ident.me +``` + +## Security Considerations + +### IP Validation + +The system validates that detected IPs are: + +- Valid IPv4 addresses +- Public (not private, loopback, or link-local) +- Properly formatted for AWS security groups + +### Network Security + +- Only your current public IP is granted access +- Additional IP ranges can be specified manually +- Security group rules are automatically configured + +## Best Practices + +1. **Use Automatic Detection**: Let Holodeck handle IP detection automatically +1. **Specify Additional Ranges**: Use `ingressIpRanges` only for additional networks +1. **Test Connectivity**: Verify access to IP detection services in your environment +1. **Monitor Changes**: Be aware that your public IP may change (DHCP, mobile networks) + +## Related Documentation + +- [Create Command](../commands/create.md#automated-ip-detection) +- [Prerequisites](../prerequisites.md#network-requirements) +- [Examples](../../examples/README.md#updated-aws-examples) diff --git a/docs/prerequisites.md b/docs/prerequisites.md index ded28ae56..b385ab9f0 100644 --- a/docs/prerequisites.md +++ b/docs/prerequisites.md @@ -73,9 +73,11 @@ spec: ## Network Requirements -- Outbound internet access for package downloads -- Appropriate security group rules for your use case -- VPC configuration if using AWS provider +- **Outbound Internet Access**: Required for package downloads and IP detection +- **IP Detection Services**: Access to public IP detection services + (ipify.org, ifconfig.me, icanhazip.com, ident.me) +- **Security Group Rules**: Automatically configured for your detected public IP +- **VPC Configuration**: Automatically configured if using AWS provider ## GPU & Driver Requirements diff --git a/examples/aws_kind.yaml b/examples/aws_kind.yaml index 7f8d99780..44a598a3d 100644 --- a/examples/aws_kind.yaml +++ b/examples/aws_kind.yaml @@ -11,8 +11,9 @@ spec: instance: type: g4dn.xlarge region: eu-north-1 - ingressIpRanges: - - / + # ingressIpRanges is now optional - your IP is detected automatically + # ingressIpRanges: + # - "YOUR_IP/32" # Only needed for additional IP ranges image: architecture: amd64 kubernetes: diff --git a/examples/aws_kubeadm.yaml b/examples/aws_kubeadm.yaml index 5d855b68c..f8c49bc1c 100644 --- a/examples/aws_kubeadm.yaml +++ b/examples/aws_kubeadm.yaml @@ -11,8 +11,9 @@ spec: instance: type: g4dn.xlarge region: us-west-1 - ingressIpRanges: - - + # ingressIpRanges is now optional - your IP is detected automatically + # ingressIpRanges: + # - "YOUR_IP/32" # Only needed for additional IP ranges image: architecture: amd64 nvidiaDriver: diff --git a/examples/v1alpha1_environment.yaml b/examples/v1alpha1_environment.yaml index 8d9a269b4..1e47aede3 100644 --- a/examples/v1alpha1_environment.yaml +++ b/examples/v1alpha1_environment.yaml @@ -11,8 +11,9 @@ spec: instance: type: g4dn.xlarge region: eu-north-1 - ingressIpRanges: - - / + # ingressIpRanges is now optional - your IP is detected automatically + # ingressIpRanges: + # - "YOUR_IP/32" # Only needed for additional IP ranges image: architecture: X86_64 containerRuntime: diff --git a/pkg/provider/aws/create.go b/pkg/provider/aws/create.go index 040b127ea..040c356e2 100644 --- a/pkg/provider/aws/create.go +++ b/pkg/provider/aws/create.go @@ -21,6 +21,8 @@ import ( "fmt" "time" + "github.com/NVIDIA/holodeck/pkg/utils" + "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/aws/aws-sdk-go/aws" @@ -248,7 +250,18 @@ func (p *Provider) createSecurityGroup(cache *AWS) error { // Enter the Ingress rules for the security group ipRanges := []types.IpRange{} - for _, ip := range p.Spec.IngresIpRanges { + // First lookup for the IP address of the user + ip, err := utils.GetIPAddress() + if err != nil { + p.fail() + return fmt.Errorf("error getting IP address: %v", err) + } + ipRanges = append(ipRanges, types.IpRange{ + CidrIp: &ip, + }) + + // Then add the IP ranges from the spec + for _, ip := range p.Spec.IngressIpRanges { ipRanges = append(ipRanges, types.IpRange{ CidrIp: &ip, }) diff --git a/pkg/utils/ip.go b/pkg/utils/ip.go new file mode 100644 index 000000000..71a61b227 --- /dev/null +++ b/pkg/utils/ip.go @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "strings" + "time" +) + +// GetIPAddress gets the IP address of the user with timeout and fallback services +func GetIPAddress() (string, error) { + // List of IP lookup services to try in order of preference + ipServices := []struct { + url string + timeout time.Duration + }{ + {"https://api.ipify.org?format=text", 5 * time.Second}, + {"https://ifconfig.me/ip", 5 * time.Second}, + {"https://icanhazip.com", 5 * time.Second}, + {"https://ident.me", 5 * time.Second}, + } + + // Create context with overall timeout + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + // Try each service until one works + for _, service := range ipServices { + ip, err := getIPFromHTTPService(ctx, service.url, service.timeout) + if err == nil && isValidPublicIP(ip) { + return fmt.Sprintf("%s/32", ip), nil + } + } + + return "", fmt.Errorf("failed to get IP address from all services") +} + +// getIPFromHTTPService attempts to get IP from a specific HTTP service +func getIPFromHTTPService(ctx context.Context, url string, timeout time.Duration) (string, error) { + // Create HTTP client with timeout + client := &http.Client{ + Timeout: timeout, + } + + // Create request with context + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return "", fmt.Errorf("error creating request for %s: %v", url, err) + } + + // Set user agent to avoid being blocked + req.Header.Set("User-Agent", "Holodeck") + + // Make the request + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("error fetching IP from %s: %v", url, err) + } + defer resp.Body.Close() // nolint:errcheck, gosec, staticcheck + + // Check status code + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("unexpected status from %s: %s", url, resp.Status) + } + + // Read response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("error reading response from %s: %v", url, err) + } + + // Clean the IP address (remove whitespace and newlines) + ip := strings.TrimSpace(string(body)) + if ip == "" { + return "", fmt.Errorf("empty response from %s", url) + } + + return ip, nil +} + +// isValidPublicIP validates that the IP is a valid public IPv4 address +func isValidPublicIP(ipStr string) bool { + ip := net.ParseIP(ipStr) + if ip == nil { + return false + } + + // Must be IPv4 + if ip.To4() == nil { + return false + } + + // Must not be private, loopback, or link-local + return !ip.IsPrivate() && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() +} diff --git a/tests/aws_test.go b/tests/aws_test.go index abe77a0f3..50d6e57d0 100644 --- a/tests/aws_test.go +++ b/tests/aws_test.go @@ -103,7 +103,6 @@ var _ = DescribeTable("AWS Environment E2E", Expect(provisioner.Dryrun(state.log, state.opts.cfg)).To(Succeed(), "Provisioner validation failed") Expect(state.opts.cfg.Spec.Instance.Type).NotTo(BeEmpty(), "Instance type should not be empty") Expect(state.opts.cfg.Spec.Instance.Region).NotTo(BeEmpty(), "Region should not be empty") - Expect(state.opts.cfg.Spec.Instance.IngresIpRanges).NotTo(BeEmpty(), "Ingress IP ranges should not be empty") By("Environment Management") // Ensure environment cleanup even if test fails diff --git a/tests/data/test_aws.yml b/tests/data/test_aws.yml index 6e04b6507..f31025f52 100644 --- a/tests/data/test_aws.yml +++ b/tests/data/test_aws.yml @@ -11,13 +11,6 @@ spec: instance: type: g4dn.xlarge region: us-west-1 - ingressIpRanges: - - 18.190.12.32/32 - - 3.143.46.93/32 - - 44.230.241.223/32 - - 44.235.4.62/32 - - 52.15.119.136/32 - - 52.24.205.48/32 image: architecture: amd64 containerRuntime: diff --git a/tests/data/test_aws_dra.yml b/tests/data/test_aws_dra.yml index b5d903d0f..89e3d014c 100644 --- a/tests/data/test_aws_dra.yml +++ b/tests/data/test_aws_dra.yml @@ -11,13 +11,6 @@ spec: instance: type: m4.xlarge region: us-west-1 - ingressIpRanges: - - 18.190.12.32/32 - - 3.143.46.93/32 - - 44.230.241.223/32 - - 44.235.4.62/32 - - 52.15.119.136/32 - - 52.24.205.48/32 image: architecture: amd64 containerRuntime: diff --git a/tests/data/test_aws_kernel.yml b/tests/data/test_aws_kernel.yml index f358b3753..1e3febdd5 100644 --- a/tests/data/test_aws_kernel.yml +++ b/tests/data/test_aws_kernel.yml @@ -11,13 +11,6 @@ spec: instance: type: g4dn.xlarge region: us-west-1 - ingressIpRanges: - - 18.190.12.32/32 - - 3.143.46.93/32 - - 44.230.241.223/32 - - 44.235.4.62/32 - - 52.15.119.136/32 - - 52.24.205.48/32 image: architecture: amd64 kernel: diff --git a/tests/data/test_aws_legacy.yml b/tests/data/test_aws_legacy.yml index 1b703742c..a811930e0 100644 --- a/tests/data/test_aws_legacy.yml +++ b/tests/data/test_aws_legacy.yml @@ -11,13 +11,6 @@ spec: instance: type: m4.xlarge region: us-west-1 - ingressIpRanges: - - 18.190.12.32/32 - - 3.143.46.93/32 - - 44.230.241.223/32 - - 44.235.4.62/32 - - 52.15.119.136/32 - - 52.24.205.48/32 image: architecture: amd64 containerRuntime: