diff --git a/docs/changelog.md b/docs/changelog.md index 3e35671..180dee3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,11 @@ # Changelog +## \[Unreleased] +### Added +* `ionoscloud-additional-lans-ids` flag to attach additional LANs to the machine by numeric ID. Values are merged with any IDs resolved from `ionoscloud-additional-lans`. +### Fixed +* `ionoscloud-additional-lans` is no longer silently ignored when the primary NIC is configured via `ionoscloud-lan-id`. Name-to-ID resolution now runs regardless of how the primary LAN is selected. + ## \[7.1.1] ### Fixed * Ensure support for child locations for ipblock reservation and image aliases diff --git a/docs/usage/options.md b/docs/usage/options.md index 790560d..f5ab944 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -28,6 +28,7 @@ Available Options for the IONOS Cloud Docker Machine Driver: | `--ionoscloud-lan-id` | Existing Ionos Cloud LAN ID (numeric) in which to create the Docker Host | | `--ionoscloud-lan-name` | Existing Ionos Cloud LAN Name (string) in which to create the Docker Host | | `--ionoscloud-additional-lans` | Names of existing IONOS Lans to connect the machine to. Names that are not found are ignored | +| `--ionoscloud-additional-lans-ids` | Numeric IDs of existing IONOS LANs to connect the machine to. Merged with any IDs resolved from `--ionoscloud-additional-lans` | | `--ionoscloud-additional-disks` | A list of disk types and sizes for additional volumes to be created on the machine, the format is DISK_TYPE:DISK_SIZE | | `--ionoscloud-disk-size` | Ionos Cloud Volume Disk-Size in GB \(10, 50, 100, 200, 400\) | | `--ionoscloud-disk-type` | Ionos Cloud Volume Disk-Type \(HDD, SSD, SSD Standard, SSD Premium, DAS\). If server type is CUBE this value is ignored and "DAS" is used, "DAS" cannot be used with ENTERPRISE servers | @@ -94,6 +95,7 @@ Environment variables are also supported for setting options. This is a list of | `--ionoscloud-lan-id` | `IONOSCLOUD_LAN_ID` | | `--ionoscloud-lan-name` | `IONOSCLOUD_LAN_NAME` | | `--ionoscloud-additional-lans` | `IONOSCLOUD_ADDITIONAL_LANS` | +| `--ionoscloud-additional-lans-ids` | `IONOSCLOUD_ADDITIONAL_LANS_IDS` | | `--ionoscloud-additional-disks` | `IONOSCLOUD_ADDITIONAL_DISKS` | | `--ionoscloud-disk-size` | `IONOSCLOUD_DISK_SIZE` | | `--ionoscloud-disk-type` | `IONOSCLOUD_DISK_TYPE` | diff --git a/ionoscloud.go b/ionoscloud.go index 831aae7..a05d149 100644 --- a/ionoscloud.go +++ b/ionoscloud.go @@ -62,6 +62,7 @@ const ( flagNatLansToGateways = "ionoscloud-nat-lans-to-gateways" flagPrivateLan = "ionoscloud-private-lan" flagAdditionalLans = "ionoscloud-additional-lans" + flagAdditionalLansIds = "ionoscloud-additional-lans-ids" flagCreateNat = "ionoscloud-create-nat" flagRKEProvisionUserData = "ionoscloud-rancher-provision-user-data" flagAppendRKECloudInit = "ionoscloud-append-rke-cloud-init" @@ -259,6 +260,11 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag { EnvVar: extflag.KebabCaseToEnvVarCase(flagAdditionalLans), Usage: "Names of existing IONOS Lans to connect the machine to. Names that are not found are ignored", }, + mcnflag.StringSliceFlag{ + Name: flagAdditionalLansIds, + EnvVar: extflag.KebabCaseToEnvVarCase(flagAdditionalLansIds), + Usage: "Numeric IDs of existing IONOS LANs to connect the machine to. Merged with any IDs resolved from --ionoscloud-additional-lans", + }, mcnflag.BoolFlag{ Name: flagWaitForIpChange, EnvVar: extflag.KebabCaseToEnvVarCase(flagWaitForIpChange), @@ -480,6 +486,14 @@ func (d *Driver) SetConfigFromFlags(opts drivers.DriverOptions) error { d.CloudInitB64 = opts.String(flagCloudInitB64) d.PrivateLan = opts.Bool(flagPrivateLan) d.AdditionalLans = opts.StringSlice(flagAdditionalLans) + d.AdditionalLansIds = nil + for _, raw := range opts.StringSlice(flagAdditionalLansIds) { + id, err := strconv.Atoi(strings.TrimSpace(raw)) + if err != nil { + return fmt.Errorf("invalid value for %s: %q must be a numeric LAN id", flagAdditionalLansIds, raw) + } + d.AdditionalLansIds = append(d.AdditionalLansIds, id) + } d.SwarmMaster = opts.Bool("swarm-master") d.SwarmHost = opts.String("swarm-host") @@ -585,7 +599,7 @@ func (d *Driver) PreCreateCheck() error { if d.DatacenterId != "" { d.DCExists = true - if d.LanId == "" { + if d.LanId == "" || len(d.AdditionalLans) > 0 { lans, err := d.client().GetLans(d.DatacenterId) if err != nil { return err @@ -593,7 +607,7 @@ func (d *Driver) PreCreateCheck() error { foundLan := false for _, lan := range *lans.Items { - if *lan.Properties.Name == d.LanName { + if d.LanId == "" && *lan.Properties.Name == d.LanName { if foundLan { return fmt.Errorf("multiple LANs with name %v found", d.LanName) } @@ -607,7 +621,9 @@ func (d *Driver) PreCreateCheck() error { if err != nil { return fmt.Errorf("invalid LAN ID found: %v", *lanId) } - d.AdditionalLansIds = append(d.AdditionalLansIds, lanIdInt) + if !slices.Contains(d.AdditionalLansIds, lanIdInt) { + d.AdditionalLansIds = append(d.AdditionalLansIds, lanIdInt) + } } } } diff --git a/ionoscloud_test.go b/ionoscloud_test.go index 982f1b1..e255ad8 100644 --- a/ionoscloud_test.go +++ b/ionoscloud_test.go @@ -536,6 +536,65 @@ func TestPreCreateLans(t *testing.T) { assert.NoError(t, err) } +// Regression: when the primary NIC is selected by --ionoscloud-lan-id, +// any names listed in --ionoscloud-additional-lans must still be resolved +// to LAN ids instead of being silently dropped. +func TestPreCreateAdditionalLansResolvedWhenLanIdSet(t *testing.T) { + driver, clientMock := NewTestDriverFlagsSet(t, authFlagsSet) + driver.DatacenterId = "test" + driver.LanId = "100" + driver.AdditionalLans = []string{lanName1, lanName2} + clientMock.EXPECT().GetLans(driver.DatacenterId).Return(&additionalLans, nil) + clientMock.EXPECT().GetLan(driver.DatacenterId, driver.LanId).Return(privateLan, nil) + clientMock.EXPECT().GetDatacenter(driver.DatacenterId).Return(dc, nil) + clientMock.EXPECT().GetImageById(defaultImageAlias).Return(&sdkgo.Image{}, fmt.Errorf("no image found with this id")) + clientMock.EXPECT().GetImages().Return(&images, nil) + clientMock.EXPECT().GetNats(driver.DatacenterId).Return(nats, nil) + err := driver.PreCreateCheck() + assert.NoError(t, err) + assert.Equal(t, "100", driver.LanId, "primary LanId must remain untouched") + assert.ElementsMatch(t, []int{lanId1Int, 5}, driver.AdditionalLansIds) +} + +// --ionoscloud-additional-lans-ids must populate AdditionalLansIds directly +// and merge with any ids resolved from --ionoscloud-additional-lans (no dupes). +func TestPreCreateAdditionalLansIdsFromFlag(t *testing.T) { + flags := map[string]interface{}{ + flagUsername: "IONOSCLOUD_USERNAME", + flagPassword: "IONOSCLOUD_PASSWORD", + flagAdditionalLans: []string{lanName1}, + flagAdditionalLansIds: []string{"2", "7"}, + } + driver, clientMock := NewTestDriverFlagsSet(t, flags) + driver.DatacenterId = "test" + driver.LanId = "100" + clientMock.EXPECT().GetLans(driver.DatacenterId).Return(&additionalLans, nil) + clientMock.EXPECT().GetLan(driver.DatacenterId, driver.LanId).Return(privateLan, nil) + clientMock.EXPECT().GetDatacenter(driver.DatacenterId).Return(dc, nil) + clientMock.EXPECT().GetImageById(defaultImageAlias).Return(&sdkgo.Image{}, fmt.Errorf("no image found with this id")) + clientMock.EXPECT().GetImages().Return(&images, nil) + clientMock.EXPECT().GetNats(driver.DatacenterId).Return(nats, nil) + err := driver.PreCreateCheck() + assert.NoError(t, err) + assert.ElementsMatch(t, []int{2, 7}, driver.AdditionalLansIds) +} + +// A non-numeric value for --ionoscloud-additional-lans-ids must fail fast +// during flag parsing rather than silently dropping the entry. +func TestSetConfigFromFlagsAdditionalLansIdsInvalid(t *testing.T) { + driver, _ := NewTestDriver(t, defaultHostName, defaultStorePath) + checkFlags := &drivers.CheckDriverOptions{ + FlagsValues: map[string]interface{}{ + flagUsername: "IONOSCLOUD_USERNAME", + flagPassword: "IONOSCLOUD_PASSWORD", + flagAdditionalLansIds: []string{"not-a-number"}, + }, + CreateFlags: driver.GetCreateFlags(), + } + err := driver.SetConfigFromFlags(checkFlags) + assert.Error(t, err) +} + func TestCreateSSHKeyErr(t *testing.T) { driver, _ := NewTestDriverFlagsSet(t, authFlagsSet) driver.SSHKey = ""