mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-02-04 09:05:31 -05:00
cli: deploy yawol as OpenStack loadbalancer
This commit is contained in:
parent
0ebe6e669d
commit
56635c3993
@ -18,6 +18,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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/k8sapi"
|
||||||
"github.com/edgelesssys/constellation/v2/bootstrapper/internal/kubernetes/kubewaiter"
|
"github.com/edgelesssys/constellation/v2/bootstrapper/internal/kubernetes/kubewaiter"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"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/logger"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/role"
|
"github.com/edgelesssys/constellation/v2/internal/role"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/versions/components"
|
"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])?)*$`)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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{
|
extraVals["ccm"] = map[string]any{
|
||||||
"OpenStack": map[string]any{
|
"OpenStack": map[string]any{
|
||||||
"secretData": credsIni,
|
"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
|
return extraVals, nil
|
||||||
}
|
}
|
||||||
@ -515,3 +534,7 @@ type constellationServicesConfig struct {
|
|||||||
cloudServiceAccountURI string
|
cloudServiceAccountURI string
|
||||||
loadBalancerIP string
|
loadBalancerIP string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type openstackMetadata interface {
|
||||||
|
GetNetworkIDs(ctx context.Context) ([]string, error)
|
||||||
|
}
|
||||||
|
@ -22,6 +22,15 @@ import (
|
|||||||
"github.com/edgelesssys/constellation/v2/internal/atls"
|
"github.com/edgelesssys/constellation/v2/internal/atls"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/compatibility"
|
"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/bootstrapper/initproto"
|
||||||
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
|
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
|
||||||
"github.com/edgelesssys/constellation/v2/cli/internal/clusterid"
|
"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/license"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/retry"
|
"github.com/edgelesssys/constellation/v2/internal/retry"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/versions"
|
"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.
|
// NewInitCmd returns a new cobra.Command for the init command.
|
||||||
|
@ -314,6 +314,26 @@ go_library(
|
|||||||
"charts/edgeless/operators/values.yaml",
|
"charts/edgeless/operators/values.yaml",
|
||||||
"charts/edgeless/constellation-services/charts/ccm/templates/openstack-daemonset.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/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",
|
importpath = "github.com/edgelesssys/constellation/v2/cli/internal/helm",
|
||||||
visibility = ["//cli:__subpackages__"],
|
visibility = ["//cli:__subpackages__"],
|
||||||
|
@ -67,3 +67,13 @@ dependencies:
|
|||||||
condition: azure.deployCSIDriver
|
condition: azure.deployCSIDriver
|
||||||
tags:
|
tags:
|
||||||
- Azure
|
- 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
|
||||||
|
@ -21,7 +21,7 @@ spec:
|
|||||||
args:
|
args:
|
||||||
- /bin/openstack-cloud-controller-manager
|
- /bin/openstack-cloud-controller-manager
|
||||||
- --cloud-provider=openstack
|
- --cloud-provider=openstack
|
||||||
- --cloud-config=/etc/config/cloud.conf
|
- --cloud-config=/etc/config/cloudprovider.conf
|
||||||
- --leader-elect=true
|
- --leader-elect=true
|
||||||
- --allocate-node-cidrs=false
|
- --allocate-node-cidrs=false
|
||||||
- -v=2
|
- -v=2
|
||||||
|
@ -5,5 +5,5 @@ metadata:
|
|||||||
name: openstackkey
|
name: openstackkey
|
||||||
namespace: {{ .Release.Namespace }}
|
namespace: {{ .Release.Namespace }}
|
||||||
data:
|
data:
|
||||||
cloud.conf: {{ .Values.OpenStack.secretData | b64enc }}
|
cloudprovider.conf: {{ .Values.OpenStack.secretData | b64enc }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
@ -16,6 +16,10 @@ gcp:
|
|||||||
azure:
|
azure:
|
||||||
deployCSIDriver: false
|
deployCSIDriver: false
|
||||||
|
|
||||||
|
# OpenStack specific configuration
|
||||||
|
openstack:
|
||||||
|
deployYawolLoadBalancer: false
|
||||||
|
|
||||||
# Set one of the tags to true to indicate which CSP you are deploying to.
|
# Set one of the tags to true to indicate which CSP you are deploying to.
|
||||||
tags:
|
tags:
|
||||||
AWS: false
|
AWS: false
|
||||||
|
@ -17,6 +17,12 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"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/cli/internal/helm/imageversion"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/compatibility"
|
"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/constants"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/deploy/helm"
|
"github.com/edgelesssys/constellation/v2/internal/deploy/helm"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/versions"
|
"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.
|
// Run `go generate` to download (and patch) upstream helm charts.
|
||||||
@ -507,6 +508,21 @@ func extendConstellationServicesValues(
|
|||||||
in["gcp"] = map[string]any{
|
in["gcp"] = map[string]any{
|
||||||
"deployCSIDriver": cfg.DeployCSIDriver(),
|
"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
|
return nil
|
||||||
|
@ -20,7 +20,7 @@ spec:
|
|||||||
args:
|
args:
|
||||||
- /bin/openstack-cloud-controller-manager
|
- /bin/openstack-cloud-controller-manager
|
||||||
- --cloud-provider=openstack
|
- --cloud-provider=openstack
|
||||||
- --cloud-config=/etc/config/cloud.conf
|
- --cloud-config=/etc/config/cloudprovider.conf
|
||||||
- --leader-elect=true
|
- --leader-elect=true
|
||||||
- --allocate-node-cidrs=false
|
- --allocate-node-cidrs=false
|
||||||
- -v=2
|
- -v=2
|
||||||
|
@ -4,4 +4,4 @@ metadata:
|
|||||||
name: openstackkey
|
name: openstackkey
|
||||||
namespace: testNamespace
|
namespace: testNamespace
|
||||||
data:
|
data:
|
||||||
cloud.conf: YmFhYWFhYWQ=
|
cloudprovider.conf: YmFhYWFhYWQ=
|
||||||
|
@ -98,7 +98,7 @@ func (k AccountKey) CloudINI() CloudINI {
|
|||||||
AuthURL: k.AuthURL,
|
AuthURL: k.AuthURL,
|
||||||
Username: k.Username,
|
Username: k.Username,
|
||||||
Password: k.Password,
|
Password: k.Password,
|
||||||
TenantID: k.ProjectID,
|
ProjectID: k.ProjectID,
|
||||||
TenantName: k.ProjectName,
|
TenantName: k.ProjectName,
|
||||||
UserDomainName: k.UserDomainName,
|
UserDomainName: k.UserDomainName,
|
||||||
TenantDomainName: k.ProjectDomainName,
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
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"`
|
TenantDomainName string `gcfg:"tenant-domain-name" mapstructure:"project-domain-name" name:"os-projectDomainName" value:"optional"`
|
||||||
Region string `name:"os-region"`
|
Region string `name:"os-region"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the string representation of the CloudINI.
|
// FullConfiguration returns the string representation of the full CloudINI.
|
||||||
func (i CloudINI) String() string {
|
func (i CloudINI) FullConfiguration() string {
|
||||||
// sanitize parameters to not include newlines
|
// sanitize parameters to not include newlines
|
||||||
authURL := newlineRegexp.ReplaceAllString(i.AuthURL, "")
|
authURL := newlineRegexp.ReplaceAllString(i.AuthURL, "")
|
||||||
username := newlineRegexp.ReplaceAllString(i.Username, "")
|
username := newlineRegexp.ReplaceAllString(i.Username, "")
|
||||||
password := newlineRegexp.ReplaceAllString(i.Password, "")
|
password := newlineRegexp.ReplaceAllString(i.Password, "")
|
||||||
tenantID := newlineRegexp.ReplaceAllString(i.TenantID, "")
|
tenantID := newlineRegexp.ReplaceAllString(i.ProjectID, "")
|
||||||
tenantName := newlineRegexp.ReplaceAllString(i.TenantName, "")
|
tenantName := newlineRegexp.ReplaceAllString(i.TenantName, "")
|
||||||
userDomainName := newlineRegexp.ReplaceAllString(i.UserDomainName, "")
|
userDomainName := newlineRegexp.ReplaceAllString(i.UserDomainName, "")
|
||||||
tenantDomainName := newlineRegexp.ReplaceAllString(i.TenantDomainName, "")
|
tenantDomainName := newlineRegexp.ReplaceAllString(i.TenantDomainName, "")
|
||||||
@ -142,4 +142,24 @@ region = %s
|
|||||||
`, authURL, username, password, tenantID, tenantName, userDomainName, tenantDomainName, region)
|
`, 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]+`)
|
var newlineRegexp = regexp.MustCompile(`[\r\n]+`)
|
||||||
|
@ -143,7 +143,7 @@ func TestAccountKeyToCloudINI(t *testing.T) {
|
|||||||
AuthURL: "auth-url",
|
AuthURL: "auth-url",
|
||||||
Username: "username",
|
Username: "username",
|
||||||
Password: "password",
|
Password: "password",
|
||||||
TenantID: "project-id",
|
ProjectID: "project-id",
|
||||||
TenantName: "project-name",
|
TenantName: "project-name",
|
||||||
UserDomainName: "user-domain-name",
|
UserDomainName: "user-domain-name",
|
||||||
TenantDomainName: "project-domain-name",
|
TenantDomainName: "project-domain-name",
|
||||||
@ -151,12 +151,12 @@ func TestAccountKeyToCloudINI(t *testing.T) {
|
|||||||
}, ini)
|
}, ini)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCloudINIToString(t *testing.T) {
|
func TestFullConfiguration(t *testing.T) {
|
||||||
ini := CloudINI{
|
ini := CloudINI{
|
||||||
AuthURL: "auth-url",
|
AuthURL: "auth-url",
|
||||||
Username: "username",
|
Username: "username",
|
||||||
Password: "password",
|
Password: "password",
|
||||||
TenantID: "project-id",
|
ProjectID: "project-id",
|
||||||
TenantName: "project-name",
|
TenantName: "project-name",
|
||||||
UserDomainName: "user-domain-name",
|
UserDomainName: "user-domain-name",
|
||||||
TenantDomainName: "project-domain-name",
|
TenantDomainName: "project-domain-name",
|
||||||
@ -171,5 +171,26 @@ tenant-name = project-name
|
|||||||
user-domain-name = user-domain-name
|
user-domain-name = user-domain-name
|
||||||
tenant-domain-name = project-domain-name
|
tenant-domain-name = project-domain-name
|
||||||
region = region-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())
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ type imdsAPI interface {
|
|||||||
initSecretHash(ctx context.Context) (string, error)
|
initSecretHash(ctx context.Context) (string, error)
|
||||||
role(ctx context.Context) (role.Role, error)
|
role(ctx context.Context) (role.Role, error)
|
||||||
vpcIP(ctx context.Context) (string, error)
|
vpcIP(ctx context.Context) (string, error)
|
||||||
|
networkIDs(ctx context.Context) ([]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type serversAPI interface {
|
type serversAPI interface {
|
||||||
|
@ -30,6 +30,8 @@ type stubIMDSClient struct {
|
|||||||
roleErr error
|
roleErr error
|
||||||
vpcIPResult string
|
vpcIPResult string
|
||||||
vpcIPErr error
|
vpcIPErr error
|
||||||
|
networkIDsResult []string
|
||||||
|
networkIDsErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *stubIMDSClient) providerID(_ context.Context) (string, 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
|
return c.vpcIPResult, c.vpcIPErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *stubIMDSClient) networkIDs(_ context.Context) ([]string, error) {
|
||||||
|
return c.networkIDsResult, c.networkIDsErr
|
||||||
|
}
|
||||||
|
|
||||||
type stubServersClient struct {
|
type stubServersClient struct {
|
||||||
serversPager stubPager
|
serversPager stubPager
|
||||||
subnetsPager stubPager
|
subnetsPager stubPager
|
||||||
|
@ -22,18 +22,21 @@ import (
|
|||||||
// documentation of OpenStack Metadata Service: https://docs.openstack.org/nova/rocky/user/metadata-service.html
|
// documentation of OpenStack Metadata Service: https://docs.openstack.org/nova/rocky/user/metadata-service.html
|
||||||
|
|
||||||
const (
|
const (
|
||||||
imdsMetaDataURL = "http://169.254.169.254/openstack/2018-08-27/meta_data.json"
|
imdsMetaDataURL = "http://169.254.169.254/openstack/2018-08-27/meta_data.json"
|
||||||
ec2ImdsBaseURL = "http://169.254.169.254/1.0/meta-data"
|
imdsNetworkDataURL = "http://169.254.169.254/openstack/2018-08-27/network_data.json"
|
||||||
maxCacheAge = 12 * time.Hour
|
ec2ImdsBaseURL = "http://169.254.169.254/1.0/meta-data"
|
||||||
|
maxCacheAge = 12 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
type imdsClient struct {
|
type imdsClient struct {
|
||||||
client httpClient
|
client httpClient
|
||||||
|
|
||||||
vpcIPCache string
|
vpcIPCache string
|
||||||
vpcIPCacheTime time.Time
|
vpcIPCacheTime time.Time
|
||||||
cache metadataResponse
|
networkCache networkResponse
|
||||||
cacheTime time.Time
|
networkCacheTime time.Time
|
||||||
|
cache metadataResponse
|
||||||
|
cacheTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// providerID returns the provider ID of the instance the function is called from.
|
// 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
|
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) {
|
func httpGet(ctx context.Context, c httpClient, url string) ([]byte, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -254,6 +281,16 @@ type metadataTags struct {
|
|||||||
Password string `json:"openstack-password,omitempty"`
|
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 {
|
type httpClient interface {
|
||||||
Do(req *http.Request) (*http.Response, error)
|
Do(req *http.Request) (*http.Response, error)
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
func TestTimeForUpdate(t *testing.T) {
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
cacheTime time.Time
|
cacheTime time.Time
|
||||||
|
@ -297,6 +297,16 @@ func (c *Cloud) GetLoadBalancerEndpoint(ctx context.Context) (string, error) {
|
|||||||
return "", errors.New("no load balancer endpoint found")
|
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) {
|
func (c *Cloud) getSubnetCIDR(uidTag string) (netip.Prefix, error) {
|
||||||
listSubnetsOpts := subnets.ListOpts{Tags: uidTag}
|
listSubnetsOpts := subnets.ListOpts{Tags: uidTag}
|
||||||
subnetsPage, err := c.api.ListSubnets(listSubnetsOpts).AllPages()
|
subnetsPage, err := c.api.ListSubnets(listSubnetsOpts).AllPages()
|
||||||
|
@ -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.
|
// newSubnetPager returns a subnet pager as we would get from a ListSubnets.
|
||||||
func newSubnetPager(nets []subnets.Subnet, err error) stubPager {
|
func newSubnetPager(nets []subnets.Subnet, err error) stubPager {
|
||||||
return stubPager{
|
return stubPager{
|
||||||
|
@ -230,6 +230,15 @@ type OpenStackConfig struct {
|
|||||||
// description: |
|
// description: |
|
||||||
// If enabled, downloads OS image directly from source URL to OpenStack. Otherwise, downloads image to local machine and uploads to OpenStack.
|
// 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"`
|
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.
|
// QEMUConfig holds config information for QEMU based Constellation deployments.
|
||||||
@ -321,7 +330,8 @@ func Default() *Config {
|
|||||||
DeployCSIDriver: toPtr(true),
|
DeployCSIDriver: toPtr(true),
|
||||||
},
|
},
|
||||||
OpenStack: &OpenStackConfig{
|
OpenStack: &OpenStackConfig{
|
||||||
DirectDownload: toPtr(true),
|
DirectDownload: toPtr(true),
|
||||||
|
DeployYawolLoadBalancer: toPtr(true),
|
||||||
},
|
},
|
||||||
QEMU: &QEMUConfig{
|
QEMU: &QEMUConfig{
|
||||||
ImageFormat: "raw",
|
ImageFormat: "raw",
|
||||||
@ -523,6 +533,11 @@ func (c *Config) DeployCSIDriver() bool {
|
|||||||
c.Provider.GCP != nil && c.Provider.GCP.DeployCSIDriver != nil && *c.Provider.GCP.DeployCSIDriver
|
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.
|
// Validate checks the config values and returns validation errors.
|
||||||
func (c *Config) Validate(force bool) error {
|
func (c *Config) Validate(force bool) error {
|
||||||
trans := ut.New(en.New()).GetFallback()
|
trans := ut.New(en.New()).GetFallback()
|
||||||
|
@ -276,7 +276,7 @@ func init() {
|
|||||||
FieldName: "openstack",
|
FieldName: "openstack",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
OpenStackConfigDoc.Fields = make([]encoder.Doc, 14)
|
OpenStackConfigDoc.Fields = make([]encoder.Doc, 17)
|
||||||
OpenStackConfigDoc.Fields[0].Name = "cloud"
|
OpenStackConfigDoc.Fields[0].Name = "cloud"
|
||||||
OpenStackConfigDoc.Fields[0].Type = "string"
|
OpenStackConfigDoc.Fields[0].Type = "string"
|
||||||
OpenStackConfigDoc.Fields[0].Note = ""
|
OpenStackConfigDoc.Fields[0].Note = ""
|
||||||
@ -347,6 +347,21 @@ func init() {
|
|||||||
OpenStackConfigDoc.Fields[13].Note = ""
|
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].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[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.Type = "QEMUConfig"
|
||||||
QEMUConfigDoc.Comments[encoder.LineComment] = "QEMUConfig holds config information for QEMU based Constellation deployments."
|
QEMUConfigDoc.Comments[encoder.LineComment] = "QEMUConfig holds config information for QEMU based Constellation deployments."
|
||||||
|
Loading…
x
Reference in New Issue
Block a user