diff --git a/bootstrapper/internal/kubernetes/kubernetes.go b/bootstrapper/internal/kubernetes/kubernetes.go index e5830497d..aa1494755 100644 --- a/bootstrapper/internal/kubernetes/kubernetes.go +++ b/bootstrapper/internal/kubernetes/kubernetes.go @@ -18,6 +18,11 @@ import ( "strings" "time" + "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" + "github.com/edgelesssys/constellation/v2/bootstrapper/internal/kubernetes/k8sapi" "github.com/edgelesssys/constellation/v2/bootstrapper/internal/kubernetes/kubewaiter" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" @@ -29,10 +34,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/role" "github.com/edgelesssys/constellation/v2/internal/versions/components" - "go.uber.org/zap" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - kubeadm "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" ) var validHostnameRegex = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) @@ -481,13 +482,31 @@ func (k *KubeWrapper) setupExtraVals(ctx context.Context, serviceConfig constell if err != nil { return nil, err } - credsIni := creds.CloudINI().String() + credsIni := creds.CloudINI().FullConfiguration() + networkIDsGetter, ok := k.providerMetadata.(openstackMetadata) + if !ok { + return nil, errors.New("generating yawol configuration: cloud provider metadata does not implement OpenStack specific methods") + } + networkIDs, err := networkIDsGetter.GetNetworkIDs(ctx) + if err != nil { + return nil, fmt.Errorf("getting network IDs: %w", err) + } + if len(networkIDs) == 0 { + return nil, errors.New("getting network IDs: no network IDs found") + } extraVals["ccm"] = map[string]any{ "OpenStack": map[string]any{ "secretData": credsIni, }, } - + yawolIni := creds.CloudINI().YawolConfiguration() + extraVals["yawol-config"] = map[string]any{ + "secretData": yawolIni, + } + extraVals["yawol-controller"] = map[string]any{ + "yawolNetworkID": networkIDs[0], + "yawolAPIHost": fmt.Sprintf("https://%s:%d", serviceConfig.loadBalancerIP, constants.KubernetesPort), + } } return extraVals, nil } @@ -515,3 +534,7 @@ type constellationServicesConfig struct { cloudServiceAccountURI string loadBalancerIP string } + +type openstackMetadata interface { + GetNetworkIDs(ctx context.Context) ([]string, error) +} diff --git a/cli/internal/cmd/init.go b/cli/internal/cmd/init.go index 487af62bb..4c565cc22 100644 --- a/cli/internal/cmd/init.go +++ b/cli/internal/cmd/init.go @@ -22,6 +22,15 @@ import ( "github.com/edgelesssys/constellation/v2/internal/atls" "github.com/edgelesssys/constellation/v2/internal/compatibility" + "github.com/spf13/afero" + "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/connectivity" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/clientcmd" + clientcodec "k8s.io/client-go/tools/clientcmd/api/latest" + "sigs.k8s.io/yaml" + "github.com/edgelesssys/constellation/v2/bootstrapper/initproto" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" "github.com/edgelesssys/constellation/v2/cli/internal/clusterid" @@ -40,14 +49,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/license" "github.com/edgelesssys/constellation/v2/internal/retry" "github.com/edgelesssys/constellation/v2/internal/versions" - "github.com/spf13/afero" - "github.com/spf13/cobra" - "google.golang.org/grpc" - "google.golang.org/grpc/connectivity" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/tools/clientcmd" - clientcodec "k8s.io/client-go/tools/clientcmd/api/latest" - "sigs.k8s.io/yaml" ) // NewInitCmd returns a new cobra.Command for the init command. diff --git a/cli/internal/helm/BUILD.bazel b/cli/internal/helm/BUILD.bazel index 54c3f5086..7c6f7c2aa 100644 --- a/cli/internal/helm/BUILD.bazel +++ b/cli/internal/helm/BUILD.bazel @@ -314,6 +314,26 @@ go_library( "charts/edgeless/operators/values.yaml", "charts/edgeless/constellation-services/charts/ccm/templates/openstack-daemonset.yaml", "charts/edgeless/constellation-services/charts/ccm/templates/openstack-secret.yaml", + "charts/edgeless/constellation-services/charts/yawol-controller/Chart.yaml", + "charts/edgeless/constellation-services/charts/yawol-controller/README.md", + "charts/edgeless/constellation-services/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancermachines.yaml", + "charts/edgeless/constellation-services/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancers.yaml", + "charts/edgeless/constellation-services/charts/yawol-controller/crds/yawol.stackit.cloud_loadbalancersets.yaml", + "charts/edgeless/constellation-services/charts/yawol-controller/templates/_helpers.tpl", + "charts/edgeless/constellation-services/charts/yawol-controller/templates/rbac-yawol-cloud-controller.yaml", + "charts/edgeless/constellation-services/charts/yawol-controller/templates/rbac-yawol-controller.yaml", + "charts/edgeless/constellation-services/charts/yawol-controller/templates/sa-yawol-cloud-controller.yaml", + "charts/edgeless/constellation-services/charts/yawol-controller/templates/sa-yawol-controller.yaml", + "charts/edgeless/constellation-services/charts/yawol-controller/templates/vpa.yaml", + "charts/edgeless/constellation-services/charts/yawol-controller/templates/yawol-cloud-controller.yaml", + "charts/edgeless/constellation-services/charts/yawol-controller/templates/yawol-controller.yaml", + "charts/edgeless/constellation-services/charts/yawol-controller/templates/yawol-gardener-monitoring.yaml", + "charts/edgeless/constellation-services/charts/yawol-controller/values.yaml", + "charts/edgeless/constellation-services/charts/yawol-config/.helmignore", + "charts/edgeless/constellation-services/charts/yawol-config/Chart.yaml", + "charts/edgeless/constellation-services/charts/yawol-config/templates/secret.yaml", + "charts/edgeless/constellation-services/charts/yawol-config/values.schema.json", + "charts/edgeless/constellation-services/charts/yawol-config/values.yaml", ], importpath = "github.com/edgelesssys/constellation/v2/cli/internal/helm", visibility = ["//cli:__subpackages__"], diff --git a/cli/internal/helm/charts/edgeless/constellation-services/Chart.yaml b/cli/internal/helm/charts/edgeless/constellation-services/Chart.yaml index 82fb9d441..b2c5fff18 100644 --- a/cli/internal/helm/charts/edgeless/constellation-services/Chart.yaml +++ b/cli/internal/helm/charts/edgeless/constellation-services/Chart.yaml @@ -67,3 +67,13 @@ dependencies: condition: azure.deployCSIDriver tags: - Azure + - name: yawol-config + version: 0.0.0 + condition: openstack.deployYawolLoadBalancer + tags: + - OpenStack + - name: yawol-controller + version: 0.0.0 + condition: openstack.deployYawolLoadBalancer + tags: + - OpenStack diff --git a/cli/internal/helm/charts/edgeless/constellation-services/charts/ccm/templates/openstack-daemonset.yaml b/cli/internal/helm/charts/edgeless/constellation-services/charts/ccm/templates/openstack-daemonset.yaml index 4b32a4bc9..740d91f8a 100644 --- a/cli/internal/helm/charts/edgeless/constellation-services/charts/ccm/templates/openstack-daemonset.yaml +++ b/cli/internal/helm/charts/edgeless/constellation-services/charts/ccm/templates/openstack-daemonset.yaml @@ -21,7 +21,7 @@ spec: args: - /bin/openstack-cloud-controller-manager - --cloud-provider=openstack - - --cloud-config=/etc/config/cloud.conf + - --cloud-config=/etc/config/cloudprovider.conf - --leader-elect=true - --allocate-node-cidrs=false - -v=2 diff --git a/cli/internal/helm/charts/edgeless/constellation-services/charts/ccm/templates/openstack-secret.yaml b/cli/internal/helm/charts/edgeless/constellation-services/charts/ccm/templates/openstack-secret.yaml index fb93b8bfa..66bebb0c3 100644 --- a/cli/internal/helm/charts/edgeless/constellation-services/charts/ccm/templates/openstack-secret.yaml +++ b/cli/internal/helm/charts/edgeless/constellation-services/charts/ccm/templates/openstack-secret.yaml @@ -5,5 +5,5 @@ metadata: name: openstackkey namespace: {{ .Release.Namespace }} data: - cloud.conf: {{ .Values.OpenStack.secretData | b64enc }} + cloudprovider.conf: {{ .Values.OpenStack.secretData | b64enc }} {{- end -}} diff --git a/cli/internal/helm/charts/edgeless/constellation-services/values.yaml b/cli/internal/helm/charts/edgeless/constellation-services/values.yaml index 26bdef388..90aac76fb 100644 --- a/cli/internal/helm/charts/edgeless/constellation-services/values.yaml +++ b/cli/internal/helm/charts/edgeless/constellation-services/values.yaml @@ -16,6 +16,10 @@ gcp: azure: deployCSIDriver: false +# OpenStack specific configuration +openstack: + deployYawolLoadBalancer: false + # Set one of the tags to true to indicate which CSP you are deploying to. tags: AWS: false diff --git a/cli/internal/helm/loader.go b/cli/internal/helm/loader.go index 519fc355b..1c1455ff5 100644 --- a/cli/internal/helm/loader.go +++ b/cli/internal/helm/loader.go @@ -17,6 +17,12 @@ import ( "path/filepath" "strings" + "github.com/pkg/errors" + "helm.sh/helm/pkg/ignore" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" + "github.com/edgelesssys/constellation/v2/cli/internal/helm/imageversion" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/compatibility" @@ -24,11 +30,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/deploy/helm" "github.com/edgelesssys/constellation/v2/internal/versions" - "github.com/pkg/errors" - "helm.sh/helm/pkg/ignore" - "helm.sh/helm/v3/pkg/chart" - "helm.sh/helm/v3/pkg/chart/loader" - "helm.sh/helm/v3/pkg/chartutil" ) // Run `go generate` to download (and patch) upstream helm charts. @@ -507,6 +508,21 @@ func extendConstellationServicesValues( in["gcp"] = map[string]any{ "deployCSIDriver": cfg.DeployCSIDriver(), } + + case cloudprovider.OpenStack: + in["openstack"] = map[string]any{ + "deployYawolLoadBalancer": cfg.DeployYawolLoadBalancer(), + } + if cfg.DeployYawolLoadBalancer() { + in["yawol-controller"] = map[string]any{ + "yawolOSSecretName": "yawolkey", + // has to be larger than ~30s to account for slow OpenStack API calls. + "openstackTimeout": "1m", + "yawolFloatingID": cfg.Provider.OpenStack.FloatingIPPoolID, + "yawolFlavorID": cfg.Provider.OpenStack.YawolFlavorID, + "yawolImageID": cfg.Provider.OpenStack.YawolImageID, + } + } } return nil diff --git a/cli/internal/helm/testdata/OpenStack/constellation-services/charts/ccm/templates/openstack-daemonset.yaml b/cli/internal/helm/testdata/OpenStack/constellation-services/charts/ccm/templates/openstack-daemonset.yaml index 2beeb4871..a8e823826 100644 --- a/cli/internal/helm/testdata/OpenStack/constellation-services/charts/ccm/templates/openstack-daemonset.yaml +++ b/cli/internal/helm/testdata/OpenStack/constellation-services/charts/ccm/templates/openstack-daemonset.yaml @@ -20,7 +20,7 @@ spec: args: - /bin/openstack-cloud-controller-manager - --cloud-provider=openstack - - --cloud-config=/etc/config/cloud.conf + - --cloud-config=/etc/config/cloudprovider.conf - --leader-elect=true - --allocate-node-cidrs=false - -v=2 diff --git a/cli/internal/helm/testdata/OpenStack/constellation-services/charts/ccm/templates/openstack-secret.yaml b/cli/internal/helm/testdata/OpenStack/constellation-services/charts/ccm/templates/openstack-secret.yaml index f0061a2ef..f87060515 100644 --- a/cli/internal/helm/testdata/OpenStack/constellation-services/charts/ccm/templates/openstack-secret.yaml +++ b/cli/internal/helm/testdata/OpenStack/constellation-services/charts/ccm/templates/openstack-secret.yaml @@ -4,4 +4,4 @@ metadata: name: openstackkey namespace: testNamespace data: - cloud.conf: YmFhYWFhYWQ= + cloudprovider.conf: YmFhYWFhYWQ= diff --git a/internal/cloud/openstack/accountkey.go b/internal/cloud/openstack/accountkey.go index 5e52f2584..277f0cc1a 100644 --- a/internal/cloud/openstack/accountkey.go +++ b/internal/cloud/openstack/accountkey.go @@ -98,7 +98,7 @@ func (k AccountKey) CloudINI() CloudINI { AuthURL: k.AuthURL, Username: k.Username, Password: k.Password, - TenantID: k.ProjectID, + ProjectID: k.ProjectID, TenantName: k.ProjectName, UserDomainName: k.UserDomainName, TenantDomainName: k.ProjectDomainName, @@ -111,20 +111,20 @@ type CloudINI struct { AuthURL string `gcfg:"auth-url" mapstructure:"auth-url" name:"os-authURL" dependsOn:"os-password|os-trustID|os-applicationCredentialSecret|os-clientCertPath"` Username string `name:"os-userName" value:"optional" dependsOn:"os-password"` Password string `name:"os-password" value:"optional" dependsOn:"os-domainID|os-domainName,os-projectID|os-projectName,os-userID|os-userName"` - TenantID string `gcfg:"tenant-id" mapstructure:"project-id" name:"os-projectID" value:"optional" dependsOn:"os-password|os-clientCertPath"` + ProjectID string `gcfg:"project-id" mapstructure:"project-id" name:"os-projectID" value:"optional" dependsOn:"os-password|os-clientCertPath"` TenantName string `gcfg:"tenant-name" mapstructure:"project-name" name:"os-projectName" value:"optional" dependsOn:"os-password|os-clientCertPath"` UserDomainName string `gcfg:"user-domain-name" mapstructure:"user-domain-name" name:"os-userDomainName" value:"optional"` TenantDomainName string `gcfg:"tenant-domain-name" mapstructure:"project-domain-name" name:"os-projectDomainName" value:"optional"` Region string `name:"os-region"` } -// String returns the string representation of the CloudINI. -func (i CloudINI) String() string { +// FullConfiguration returns the string representation of the full CloudINI. +func (i CloudINI) FullConfiguration() string { // sanitize parameters to not include newlines authURL := newlineRegexp.ReplaceAllString(i.AuthURL, "") username := newlineRegexp.ReplaceAllString(i.Username, "") password := newlineRegexp.ReplaceAllString(i.Password, "") - tenantID := newlineRegexp.ReplaceAllString(i.TenantID, "") + tenantID := newlineRegexp.ReplaceAllString(i.ProjectID, "") tenantName := newlineRegexp.ReplaceAllString(i.TenantName, "") userDomainName := newlineRegexp.ReplaceAllString(i.UserDomainName, "") tenantDomainName := newlineRegexp.ReplaceAllString(i.TenantDomainName, "") @@ -142,4 +142,24 @@ region = %s `, authURL, username, password, tenantID, tenantName, userDomainName, tenantDomainName, region) } +// YawolConfiguration returns the string representation of the CloudINI subset yawol expects. +func (i CloudINI) YawolConfiguration() string { + // sanitize parameters to not include newlines + authURL := newlineRegexp.ReplaceAllString(i.AuthURL, "") + username := newlineRegexp.ReplaceAllString(i.Username, "") + password := newlineRegexp.ReplaceAllString(i.Password, "") + projectID := newlineRegexp.ReplaceAllString(i.ProjectID, "") + userDomainName := newlineRegexp.ReplaceAllString(i.UserDomainName, "") + region := newlineRegexp.ReplaceAllString(i.Region, "") + + return fmt.Sprintf(`[Global] +auth-url = %s +username = %s +password = %s +project-id = %s +domain-name = %s +region = %s +`, authURL, username, password, projectID, userDomainName, region) +} + var newlineRegexp = regexp.MustCompile(`[\r\n]+`) diff --git a/internal/cloud/openstack/accountkey_test.go b/internal/cloud/openstack/accountkey_test.go index bdf5e51de..b46ce14a5 100644 --- a/internal/cloud/openstack/accountkey_test.go +++ b/internal/cloud/openstack/accountkey_test.go @@ -143,7 +143,7 @@ func TestAccountKeyToCloudINI(t *testing.T) { AuthURL: "auth-url", Username: "username", Password: "password", - TenantID: "project-id", + ProjectID: "project-id", TenantName: "project-name", UserDomainName: "user-domain-name", TenantDomainName: "project-domain-name", @@ -151,12 +151,12 @@ func TestAccountKeyToCloudINI(t *testing.T) { }, ini) } -func TestCloudINIToString(t *testing.T) { +func TestFullConfiguration(t *testing.T) { ini := CloudINI{ AuthURL: "auth-url", Username: "username", Password: "password", - TenantID: "project-id", + ProjectID: "project-id", TenantName: "project-name", UserDomainName: "user-domain-name", TenantDomainName: "project-domain-name", @@ -171,5 +171,26 @@ tenant-name = project-name user-domain-name = user-domain-name tenant-domain-name = project-domain-name region = region-name -`, ini.String()) +`, ini.FullConfiguration()) +} + +func TestYawolConfiguration(t *testing.T) { + ini := CloudINI{ + AuthURL: "auth-url", + Username: "username", + Password: "password", + ProjectID: "project-id", + TenantName: "project-name", + UserDomainName: "user-domain-name", + TenantDomainName: "project-domain-name", + Region: "region-name", + } + assert.Equal(t, `[Global] +auth-url = auth-url +username = username +password = password +project-id = project-id +domain-name = user-domain-name +region = region-name +`, ini.YawolConfiguration()) } diff --git a/internal/cloud/openstack/api.go b/internal/cloud/openstack/api.go index 62a73a122..e66c31498 100644 --- a/internal/cloud/openstack/api.go +++ b/internal/cloud/openstack/api.go @@ -23,6 +23,7 @@ type imdsAPI interface { initSecretHash(ctx context.Context) (string, error) role(ctx context.Context) (role.Role, error) vpcIP(ctx context.Context) (string, error) + networkIDs(ctx context.Context) ([]string, error) } type serversAPI interface { diff --git a/internal/cloud/openstack/api_test.go b/internal/cloud/openstack/api_test.go index 11d1c1fcf..b6f623296 100644 --- a/internal/cloud/openstack/api_test.go +++ b/internal/cloud/openstack/api_test.go @@ -30,6 +30,8 @@ type stubIMDSClient struct { roleErr error vpcIPResult string vpcIPErr error + networkIDsResult []string + networkIDsErr error } func (c *stubIMDSClient) providerID(_ context.Context) (string, error) { @@ -60,6 +62,10 @@ func (c *stubIMDSClient) vpcIP(_ context.Context) (string, error) { return c.vpcIPResult, c.vpcIPErr } +func (c *stubIMDSClient) networkIDs(_ context.Context) ([]string, error) { + return c.networkIDsResult, c.networkIDsErr +} + type stubServersClient struct { serversPager stubPager subnetsPager stubPager diff --git a/internal/cloud/openstack/imds.go b/internal/cloud/openstack/imds.go index b4cabe5d6..01e31268b 100644 --- a/internal/cloud/openstack/imds.go +++ b/internal/cloud/openstack/imds.go @@ -22,18 +22,21 @@ import ( // documentation of OpenStack Metadata Service: https://docs.openstack.org/nova/rocky/user/metadata-service.html const ( - imdsMetaDataURL = "http://169.254.169.254/openstack/2018-08-27/meta_data.json" - ec2ImdsBaseURL = "http://169.254.169.254/1.0/meta-data" - maxCacheAge = 12 * time.Hour + imdsMetaDataURL = "http://169.254.169.254/openstack/2018-08-27/meta_data.json" + imdsNetworkDataURL = "http://169.254.169.254/openstack/2018-08-27/network_data.json" + ec2ImdsBaseURL = "http://169.254.169.254/1.0/meta-data" + maxCacheAge = 12 * time.Hour ) type imdsClient struct { client httpClient - vpcIPCache string - vpcIPCacheTime time.Time - cache metadataResponse - cacheTime time.Time + vpcIPCache string + vpcIPCacheTime time.Time + networkCache networkResponse + networkCacheTime time.Time + cache metadataResponse + cacheTime time.Time } // providerID returns the provider ID of the instance the function is called from. @@ -223,6 +226,30 @@ func (c *imdsClient) vpcIP(ctx context.Context) (string, error) { return c.vpcIPCache, nil } +func (c *imdsClient) networkIDs(ctx context.Context) ([]string, error) { + if c.timeForUpdate(c.networkCacheTime) || len(c.networkCache.Networks) == 0 { + resp, err := httpGet(ctx, c.client, imdsNetworkDataURL) + if err != nil { + return nil, err + } + var networkResp networkResponse + if err := json.Unmarshal(resp, &networkResp); err != nil { + return nil, err + } + c.networkCache = networkResp + c.networkCacheTime = time.Now() + } + + var networkIDs []string + for _, network := range c.networkCache.Networks { + if network.NetworkID == "" { + continue + } + networkIDs = append(networkIDs, network.NetworkID) + } + return networkIDs, nil +} + func httpGet(ctx context.Context, c httpClient, url string) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) if err != nil { @@ -254,6 +281,16 @@ type metadataTags struct { Password string `json:"openstack-password,omitempty"` } +// networkResponse contains networkResponse with only the required values. +type networkResponse struct { + Networks []metadataNetwork `json:"networks,omitempty"` +} + +type metadataNetwork struct { + ID string `json:"id,omitempty"` + NetworkID string `json:"network_id,omitempty"` +} + type httpClient interface { Do(req *http.Request) (*http.Response, error) } diff --git a/internal/cloud/openstack/imds_test.go b/internal/cloud/openstack/imds_test.go index 57430bb8c..5d5e0d25a 100644 --- a/internal/cloud/openstack/imds_test.go +++ b/internal/cloud/openstack/imds_test.go @@ -338,6 +338,124 @@ func TestVPCIP(t *testing.T) { } } +func TestNetworkIDs(t *testing.T) { + someErr := errors.New("failed") + + testCases := map[string]struct { + cache networkResponse + cacheTime time.Time + client *stubHTTPClient + wantResult []string + wantCall bool + wantErr bool + }{ + "cached": { + cache: networkResponse{Networks: []metadataNetwork{ + {ID: "net0", NetworkID: "0000000-00000-0000-0000-000000000000"}, + {ID: "net1", NetworkID: "1111111-11111-1111-1111-111111111111"}, + {ID: "invalid"}, + }}, + cacheTime: time.Now(), + wantResult: []string{ + "0000000-00000-0000-0000-000000000000", + "1111111-11111-1111-1111-111111111111", + }, + wantCall: false, + }, + "from http": { + client: &stubHTTPClient{ + response: ` + { + "networks": [ + { + "id": "net0", + "network_id": "0000000-00000-0000-0000-000000000000" + }, + { + "id": "net1", + "network_id": "1111111-11111-1111-1111-111111111111" + } + ] + }`, + }, + wantResult: []string{ + "0000000-00000-0000-0000-000000000000", + "1111111-11111-1111-1111-111111111111", + }, + wantCall: true, + }, + "cache outdated": { + cache: networkResponse{Networks: []metadataNetwork{ + {ID: "net0", NetworkID: "0000000-00000-0000-0000-000000000000"}, + }}, + cacheTime: time.Now().AddDate(0, 0, -1), + client: &stubHTTPClient{ + response: ` + { + "networks": [ + { + "id": "net1", + "network_id": "1111111-11111-1111-1111-111111111111" + } + ] + }`, + }, + wantResult: []string{"1111111-11111-1111-1111-111111111111"}, + wantCall: true, + }, + "cache empty": { + cacheTime: time.Now(), + client: &stubHTTPClient{ + response: ` + { + "networks": [ + { + "id": "net0", + "network_id": "0000000-00000-0000-0000-000000000000" + } + ] + }`, + }, + wantResult: []string{"0000000-00000-0000-0000-000000000000"}, + wantCall: true, + }, + "http error": { + client: &stubHTTPClient{err: someErr}, + wantCall: true, + wantErr: true, + }, + "http empty response": { + client: &stubHTTPClient{}, + wantCall: true, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + imds := &imdsClient{ + client: tc.client, + networkCache: tc.cache, + networkCacheTime: tc.cacheTime, + } + + result, err := imds.networkIDs(context.Background()) + + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tc.wantResult, result) + if tc.client != nil { + assert.Equal(tc.wantCall, tc.client.called) + } + } + }) + } +} + func TestTimeForUpdate(t *testing.T) { testCases := map[string]struct { cacheTime time.Time diff --git a/internal/cloud/openstack/openstack.go b/internal/cloud/openstack/openstack.go index 700d6fe6b..a996cb7b4 100644 --- a/internal/cloud/openstack/openstack.go +++ b/internal/cloud/openstack/openstack.go @@ -297,6 +297,16 @@ func (c *Cloud) GetLoadBalancerEndpoint(ctx context.Context) (string, error) { return "", errors.New("no load balancer endpoint found") } +// GetNetworkIDs returns the IDs of the networks the current instance is part of. +// This method is OpenStack specific. +func (c *Cloud) GetNetworkIDs(ctx context.Context) ([]string, error) { + networkIDs, err := c.imds.networkIDs(ctx) + if err != nil { + return nil, fmt.Errorf("retrieving network IDs: %w", err) + } + return networkIDs, nil +} + func (c *Cloud) getSubnetCIDR(uidTag string) (netip.Prefix, error) { listSubnetsOpts := subnets.ListOpts{Tags: uidTag} subnetsPage, err := c.api.ListSubnets(listSubnetsOpts).AllPages() diff --git a/internal/cloud/openstack/openstack_test.go b/internal/cloud/openstack/openstack_test.go index 46e12a882..a0478d829 100644 --- a/internal/cloud/openstack/openstack_test.go +++ b/internal/cloud/openstack/openstack_test.go @@ -653,6 +653,46 @@ func TestGetLoadBalancerEndpoint(t *testing.T) { } } +func TestGetNetworkIDs(t *testing.T) { + someErr := fmt.Errorf("failed") + + testCases := map[string]struct { + imds imdsAPI + want []string + wantErr bool + }{ + "success": { + imds: &stubIMDSClient{ + networkIDsResult: []string{"id1", "id2"}, + }, + want: []string{"id1", "id2"}, + }, + "fail to get network IDs": { + imds: &stubIMDSClient{ + networkIDsErr: someErr, + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + c := &Cloud{imds: tc.imds} + + got, err := c.GetNetworkIDs(context.Background()) + + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + assert.Equal(tc.want, got) + } + }) + } +} + // newSubnetPager returns a subnet pager as we would get from a ListSubnets. func newSubnetPager(nets []subnets.Subnet, err error) stubPager { return stubPager{ diff --git a/internal/config/config.go b/internal/config/config.go index 79aa32c65..e1875bd6b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -230,6 +230,15 @@ type OpenStackConfig struct { // description: | // If enabled, downloads OS image directly from source URL to OpenStack. Otherwise, downloads image to local machine and uploads to OpenStack. DirectDownload *bool `yaml:"directDownload" validate:"required"` + // description: | + // Deploy Yawol loadbalancer. For details see: https://github.com/stackitcloud/yawol + DeployYawolLoadBalancer *bool `yaml:"deployYawolLoadBalancer" validate:"required"` + // description: | + // OpenStack OS image used by the yawollet. For details see: https://github.com/stackitcloud/yawol + YawolImageID string `yaml:"yawolImageID"` + // description: | + // OpenStack flavor id used for yawollets. For details see: https://github.com/stackitcloud/yawol + YawolFlavorID string `yaml:"yawolFlavorID"` } // QEMUConfig holds config information for QEMU based Constellation deployments. @@ -321,7 +330,8 @@ func Default() *Config { DeployCSIDriver: toPtr(true), }, OpenStack: &OpenStackConfig{ - DirectDownload: toPtr(true), + DirectDownload: toPtr(true), + DeployYawolLoadBalancer: toPtr(true), }, QEMU: &QEMUConfig{ ImageFormat: "raw", @@ -523,6 +533,11 @@ func (c *Config) DeployCSIDriver() bool { c.Provider.GCP != nil && c.Provider.GCP.DeployCSIDriver != nil && *c.Provider.GCP.DeployCSIDriver } +// DeployYawolLoadBalancer returns whether the Yawol load balancer should be deployed. +func (c *Config) DeployYawolLoadBalancer() bool { + return c.Provider.OpenStack != nil && c.Provider.OpenStack.DeployYawolLoadBalancer != nil && *c.Provider.OpenStack.DeployYawolLoadBalancer +} + // Validate checks the config values and returns validation errors. func (c *Config) Validate(force bool) error { trans := ut.New(en.New()).GetFallback() diff --git a/internal/config/config_doc.go b/internal/config/config_doc.go index 8581a23a9..c94ed39f3 100644 --- a/internal/config/config_doc.go +++ b/internal/config/config_doc.go @@ -276,7 +276,7 @@ func init() { FieldName: "openstack", }, } - OpenStackConfigDoc.Fields = make([]encoder.Doc, 14) + OpenStackConfigDoc.Fields = make([]encoder.Doc, 17) OpenStackConfigDoc.Fields[0].Name = "cloud" OpenStackConfigDoc.Fields[0].Type = "string" OpenStackConfigDoc.Fields[0].Note = "" @@ -347,6 +347,21 @@ func init() { OpenStackConfigDoc.Fields[13].Note = "" OpenStackConfigDoc.Fields[13].Description = "If enabled, downloads OS image directly from source URL to OpenStack. Otherwise, downloads image to local machine and uploads to OpenStack." OpenStackConfigDoc.Fields[13].Comments[encoder.LineComment] = "If enabled, downloads OS image directly from source URL to OpenStack. Otherwise, downloads image to local machine and uploads to OpenStack." + OpenStackConfigDoc.Fields[14].Name = "deployYawolLoadBalancer" + OpenStackConfigDoc.Fields[14].Type = "bool" + OpenStackConfigDoc.Fields[14].Note = "" + OpenStackConfigDoc.Fields[14].Description = "Deploy Yawol loadbalancer. For details see: https://github.com/stackitcloud/yawol" + OpenStackConfigDoc.Fields[14].Comments[encoder.LineComment] = "Deploy Yawol loadbalancer. For details see: https://github.com/stackitcloud/yawol" + OpenStackConfigDoc.Fields[15].Name = "yawolImageID" + OpenStackConfigDoc.Fields[15].Type = "string" + OpenStackConfigDoc.Fields[15].Note = "" + OpenStackConfigDoc.Fields[15].Description = "OpenStack OS image used by the yawollet. For details see: https://github.com/stackitcloud/yawol" + OpenStackConfigDoc.Fields[15].Comments[encoder.LineComment] = "OpenStack OS image used by the yawollet. For details see: https://github.com/stackitcloud/yawol" + OpenStackConfigDoc.Fields[16].Name = "yawolFlavorID" + OpenStackConfigDoc.Fields[16].Type = "string" + OpenStackConfigDoc.Fields[16].Note = "" + OpenStackConfigDoc.Fields[16].Description = "OpenStack flavor id used for yawollets. For details see: https://github.com/stackitcloud/yawol" + OpenStackConfigDoc.Fields[16].Comments[encoder.LineComment] = "OpenStack flavor id used for yawollets. For details see: https://github.com/stackitcloud/yawol" QEMUConfigDoc.Type = "QEMUConfig" QEMUConfigDoc.Comments[encoder.LineComment] = "QEMUConfig holds config information for QEMU based Constellation deployments."