cli: install helm charts in cli instead of bootstrapper (#2136)

* init

* fixup! init

* gcp working?

* fixup! fixup! init

* azure cfg for microService installation

* fixup! azure cfg for microService installation

* fixup! azure cfg for microService installation

* cleanup bootstrapper code

* cleanup helminstall code

* fixup! cleanup helminstall code

* Update internal/deploy/helm/install.go

Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>

* daniel feedback

* TODO add provider (also to CreateCluster) so we can ensure that provider specific output

* fixup! daniel feedback

* use debugLog in helm installer

* placeholderHelmInstaller

* rename to stub

---------

Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>
This commit is contained in:
Adrian Stobbe 2023-07-31 10:53:05 +02:00 committed by GitHub
parent ef60d00a60
commit 26305e8f80
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 775 additions and 970 deletions

View file

@ -78,172 +78,8 @@ func (c *Client) WithManualStateMigration(migration StateMigration) *Client {
return c
}
// Show reads the default state path and outputs the state.
func (c *Client) Show(ctx context.Context) (*tfjson.State, error) {
return c.tf.Show(ctx)
}
// PrepareWorkspace prepares a Terraform workspace for a Constellation cluster.
func (c *Client) PrepareWorkspace(path string, vars Variables) error {
if err := prepareWorkspace(path, c.file, c.workingDir); err != nil {
return fmt.Errorf("prepare workspace: %w", err)
}
return c.writeVars(vars)
}
// PrepareUpgradeWorkspace prepares a Terraform workspace for a Constellation version upgrade.
// It copies the Terraform state from the old working dir and the embedded Terraform files into the new working dir.
func (c *Client) PrepareUpgradeWorkspace(path, oldWorkingDir, newWorkingDir, backupDir string, vars Variables) error {
if err := prepareUpgradeWorkspace(path, c.file, oldWorkingDir, newWorkingDir, backupDir); err != nil {
return fmt.Errorf("prepare upgrade workspace: %w", err)
}
return c.writeVars(vars)
}
// PrepareIAMUpgradeWorkspace prepares a Terraform workspace for a Constellation IAM upgrade.
func PrepareIAMUpgradeWorkspace(file file.Handler, path, oldWorkingDir, newWorkingDir, backupDir string) error {
if err := prepareUpgradeWorkspace(path, file, oldWorkingDir, newWorkingDir, backupDir); err != nil {
return fmt.Errorf("prepare upgrade workspace: %w", err)
}
// copy the vars file from the old working dir to the new working dir
if err := file.CopyFile(filepath.Join(oldWorkingDir, terraformVarsFile), filepath.Join(newWorkingDir, terraformVarsFile)); err != nil {
return fmt.Errorf("copying vars file: %w", err)
}
return nil
}
// CreateCluster creates a Constellation cluster using Terraform.
func (c *Client) CreateCluster(ctx context.Context, logLevel LogLevel) (ApplyOutput, error) {
if err := c.setLogLevel(logLevel); err != nil {
return ApplyOutput{}, fmt.Errorf("set terraform log level %s: %w", logLevel.String(), err)
}
if err := c.tf.Init(ctx); err != nil {
return ApplyOutput{}, fmt.Errorf("terraform init: %w", err)
}
if err := c.applyManualStateMigrations(ctx); err != nil {
return ApplyOutput{}, fmt.Errorf("apply manual state migrations: %w", err)
}
if err := c.tf.Apply(ctx); err != nil {
return ApplyOutput{}, fmt.Errorf("terraform apply: %w", err)
}
tfState, err := c.tf.Show(ctx)
if err != nil {
return ApplyOutput{}, fmt.Errorf("terraform show: %w", err)
}
ipOutput, ok := tfState.Values.Outputs["ip"]
if !ok {
return ApplyOutput{}, errors.New("no IP output found")
}
ip, ok := ipOutput.Value.(string)
if !ok {
return ApplyOutput{}, errors.New("invalid type in IP output: not a string")
}
apiServerCertSANsOutput, ok := tfState.Values.Outputs["api_server_cert_sans"]
if !ok {
return ApplyOutput{}, errors.New("no api_server_cert_sans output found")
}
apiServerCertSANsUntyped, ok := apiServerCertSANsOutput.Value.([]any)
if !ok {
return ApplyOutput{}, fmt.Errorf("invalid type in api_server_cert_sans output: %s is not a list of elements", apiServerCertSANsOutput.Type.FriendlyName())
}
apiServerCertSANs, err := toStringSlice(apiServerCertSANsUntyped)
if err != nil {
return ApplyOutput{}, fmt.Errorf("convert api_server_cert_sans output: %w", err)
}
secretOutput, ok := tfState.Values.Outputs["initSecret"]
if !ok {
return ApplyOutput{}, errors.New("no initSecret output found")
}
secret, ok := secretOutput.Value.(string)
if !ok {
return ApplyOutput{}, errors.New("invalid type in initSecret output: not a string")
}
uidOutput, ok := tfState.Values.Outputs["uid"]
if !ok {
return ApplyOutput{}, errors.New("no uid output found")
}
uid, ok := uidOutput.Value.(string)
if !ok {
return ApplyOutput{}, errors.New("invalid type in uid output: not a string")
}
var attestationURL string
if attestationURLOutput, ok := tfState.Values.Outputs["attestationURL"]; ok {
if attestationURLString, ok := attestationURLOutput.Value.(string); ok {
attestationURL = attestationURLString
}
}
return ApplyOutput{
IP: ip,
APIServerCertSANs: apiServerCertSANs,
Secret: secret,
UID: uid,
AttestationURL: attestationURL,
}, nil
}
// ApplyOutput contains the Terraform output values of a cluster creation
// or apply operation.
type ApplyOutput struct {
IP string
APIServerCertSANs []string
Secret string
UID string
// AttestationURL is the URL of the attestation provider.
// It is only set if the cluster is created on Azure.
AttestationURL string
}
// IAMOutput contains the output information of the Terraform IAM operations.
type IAMOutput struct {
GCP GCPIAMOutput
Azure AzureIAMOutput
AWS AWSIAMOutput
}
// GCPIAMOutput contains the output information of the Terraform IAM operation on GCP.
type GCPIAMOutput struct {
SaKey string
}
// AzureIAMOutput contains the output information of the Terraform IAM operation on Microsoft Azure.
type AzureIAMOutput struct {
SubscriptionID string
TenantID string
UAMIID string
}
// AWSIAMOutput contains the output information of the Terraform IAM operation on GCP.
type AWSIAMOutput struct {
ControlPlaneInstanceProfile string
WorkerNodeInstanceProfile string
}
// ApplyIAMConfig creates an IAM configuration using Terraform.
func (c *Client) ApplyIAMConfig(ctx context.Context, provider cloudprovider.Provider, logLevel LogLevel) (IAMOutput, error) {
if err := c.setLogLevel(logLevel); err != nil {
return IAMOutput{}, fmt.Errorf("set terraform log level %s: %w", logLevel.String(), err)
}
if err := c.tf.Init(ctx); err != nil {
return IAMOutput{}, err
}
if err := c.tf.Apply(ctx); err != nil {
return IAMOutput{}, err
}
// ShowIAM reads the state of Constellation IAM resources from Terraform.
func (c *Client) ShowIAM(ctx context.Context, provider cloudprovider.Provider) (IAMOutput, error) {
tfState, err := c.tf.Show(ctx)
if err != nil {
return IAMOutput{}, err
@ -324,6 +160,277 @@ func (c *Client) ApplyIAMConfig(ctx context.Context, provider cloudprovider.Prov
}
}
// ShowCluster reads the state of Constellation cluster resources from Terraform.
func (c *Client) ShowCluster(ctx context.Context, provider cloudprovider.Provider) (ApplyOutput, error) {
tfState, err := c.tf.Show(ctx)
if err != nil {
return ApplyOutput{}, fmt.Errorf("terraform show: %w", err)
}
ipOutput, ok := tfState.Values.Outputs["ip"]
if !ok {
return ApplyOutput{}, errors.New("no IP output found")
}
ip, ok := ipOutput.Value.(string)
if !ok {
return ApplyOutput{}, errors.New("invalid type in IP output: not a string")
}
apiServerCertSANsOutput, ok := tfState.Values.Outputs["api_server_cert_sans"]
if !ok {
return ApplyOutput{}, errors.New("no api_server_cert_sans output found")
}
apiServerCertSANsUntyped, ok := apiServerCertSANsOutput.Value.([]any)
if !ok {
return ApplyOutput{}, fmt.Errorf("invalid type in api_server_cert_sans output: %s is not a list of elements", apiServerCertSANsOutput.Type.FriendlyName())
}
apiServerCertSANs, err := toStringSlice(apiServerCertSANsUntyped)
if err != nil {
return ApplyOutput{}, fmt.Errorf("convert api_server_cert_sans output: %w", err)
}
secretOutput, ok := tfState.Values.Outputs["initSecret"]
if !ok {
return ApplyOutput{}, errors.New("no initSecret output found")
}
secret, ok := secretOutput.Value.(string)
if !ok {
return ApplyOutput{}, errors.New("invalid type in initSecret output: not a string")
}
uidOutput, ok := tfState.Values.Outputs["uid"]
if !ok {
return ApplyOutput{}, errors.New("no uid output found")
}
uid, ok := uidOutput.Value.(string)
if !ok {
return ApplyOutput{}, errors.New("invalid type in uid output: not a string")
}
res := ApplyOutput{
IP: ip,
APIServerCertSANs: apiServerCertSANs,
Secret: secret,
UID: uid,
}
// TODO add provider
switch provider {
case cloudprovider.GCP:
gcpProjectOutput, ok := tfState.Values.Outputs["project"]
if ok {
gcpProject, ok := gcpProjectOutput.Value.(string)
if !ok {
return ApplyOutput{}, errors.New("invalid type in project output: not a string")
}
cidrNodesOutput, ok := tfState.Values.Outputs["ip_cidr_nodes"]
if !ok {
return ApplyOutput{}, errors.New("no ip_cidr_nodes output found")
}
cidrNodes, ok := cidrNodesOutput.Value.(string)
if !ok {
return ApplyOutput{}, errors.New("invalid type in ip_cidr_nodes output: not a string")
}
cidrPodsOutput, ok := tfState.Values.Outputs["ip_cidr_pods"]
if !ok {
return ApplyOutput{}, errors.New("no ip_cidr_pods output found")
}
cidrPods, ok := cidrPodsOutput.Value.(string)
if !ok {
return ApplyOutput{}, errors.New("invalid type in ip_cidr_pods output: not a string")
}
res.GCP = &GCPApplyOutput{
ProjectID: gcpProject,
IPCidrNode: cidrNodes,
IPCidrPod: cidrPods,
}
}
case cloudprovider.Azure:
var attestationURL string
if ok {
if attestationURLOutput, ok := tfState.Values.Outputs["attestationURL"]; ok {
if attestationURLString, ok := attestationURLOutput.Value.(string); ok {
attestationURL = attestationURLString
}
}
}
azureUAMIOutput, ok := tfState.Values.Outputs["user_assigned_identity"]
if !ok {
return ApplyOutput{}, errors.New("no user_assigned_identity output found")
}
azureUAMI, ok := azureUAMIOutput.Value.(string)
if !ok {
return ApplyOutput{}, errors.New("invalid type in user_assigned_identity output: not a string")
}
rgOutput, ok := tfState.Values.Outputs["resource_group"]
if !ok {
return ApplyOutput{}, errors.New("no resource_group output found")
}
rg, ok := rgOutput.Value.(string)
if !ok {
return ApplyOutput{}, errors.New("invalid type in resource_group output: not a string")
}
subscriptionOutput, ok := tfState.Values.Outputs["subscription_id"]
if !ok {
return ApplyOutput{}, errors.New("no subscription_id output found")
}
subscriptionID, ok := subscriptionOutput.Value.(string)
if !ok {
return ApplyOutput{}, errors.New("invalid type in subscription_id output: not a string")
}
networkSGNameOutput, ok := tfState.Values.Outputs["network_security_group_name"]
if !ok {
return ApplyOutput{}, errors.New("no network_security_group_name output found")
}
networkSGName, ok := networkSGNameOutput.Value.(string)
if !ok {
return ApplyOutput{}, errors.New("invalid type in network_security_group_name output: not a string")
}
loadBalancerNameOutput, ok := tfState.Values.Outputs["loadbalancer_name"]
if !ok {
return ApplyOutput{}, errors.New("no loadbalancer_name output found")
}
loadBalancerName, ok := loadBalancerNameOutput.Value.(string)
if !ok {
return ApplyOutput{}, errors.New("invalid type in loadbalancer_name output: not a string")
}
res.Azure = &AzureApplyOutput{
ResourceGroup: rg,
SubscriptionID: subscriptionID,
UserAssignedIdentity: azureUAMI,
NetworkSecurityGroupName: networkSGName,
LoadBalancerName: loadBalancerName,
AttestationURL: attestationURL,
}
}
return res, nil
}
// PrepareWorkspace prepares a Terraform workspace for a Constellation cluster.
func (c *Client) PrepareWorkspace(path string, vars Variables) error {
if err := prepareWorkspace(path, c.file, c.workingDir); err != nil {
return fmt.Errorf("prepare workspace: %w", err)
}
return c.writeVars(vars)
}
// PrepareUpgradeWorkspace prepares a Terraform workspace for a Constellation version upgrade.
// It copies the Terraform state from the old working dir and the embedded Terraform files into the new working dir.
func (c *Client) PrepareUpgradeWorkspace(path, oldWorkingDir, newWorkingDir, backupDir string, vars Variables) error {
if err := prepareUpgradeWorkspace(path, c.file, oldWorkingDir, newWorkingDir, backupDir); err != nil {
return fmt.Errorf("prepare upgrade workspace: %w", err)
}
return c.writeVars(vars)
}
// PrepareIAMUpgradeWorkspace prepares a Terraform workspace for a Constellation IAM upgrade.
func PrepareIAMUpgradeWorkspace(file file.Handler, path, oldWorkingDir, newWorkingDir, backupDir string) error {
if err := prepareUpgradeWorkspace(path, file, oldWorkingDir, newWorkingDir, backupDir); err != nil {
return fmt.Errorf("prepare upgrade workspace: %w", err)
}
// copy the vars file from the old working dir to the new working dir
if err := file.CopyFile(filepath.Join(oldWorkingDir, terraformVarsFile), filepath.Join(newWorkingDir, terraformVarsFile)); err != nil {
return fmt.Errorf("copying vars file: %w", err)
}
return nil
}
// CreateCluster creates a Constellation cluster using Terraform.
func (c *Client) CreateCluster(ctx context.Context, provider cloudprovider.Provider, logLevel LogLevel) (ApplyOutput, error) {
if err := c.setLogLevel(logLevel); err != nil {
return ApplyOutput{}, fmt.Errorf("set terraform log level %s: %w", logLevel.String(), err)
}
if err := c.tf.Init(ctx); err != nil {
return ApplyOutput{}, fmt.Errorf("terraform init: %w", err)
}
if err := c.applyManualStateMigrations(ctx); err != nil {
return ApplyOutput{}, fmt.Errorf("apply manual state migrations: %w", err)
}
if err := c.tf.Apply(ctx); err != nil {
return ApplyOutput{}, fmt.Errorf("terraform apply: %w", err)
}
return c.ShowCluster(ctx, provider)
}
// ApplyOutput contains the Terraform output values of a cluster creation
// or apply operation.
type ApplyOutput struct {
IP string
APIServerCertSANs []string
Secret string
UID string
GCP *GCPApplyOutput
Azure *AzureApplyOutput
}
// AzureApplyOutput contains the Terraform output values of a terraform apply operation on Microsoft Azure.
type AzureApplyOutput struct {
ResourceGroup string
SubscriptionID string
NetworkSecurityGroupName string
LoadBalancerName string
UserAssignedIdentity string
// AttestationURL is the URL of the attestation provider.
AttestationURL string
}
// GCPApplyOutput contains the Terraform output values of a terraform apply operation on GCP.
type GCPApplyOutput struct {
ProjectID string
IPCidrNode string
IPCidrPod string
}
// IAMOutput contains the output information of the Terraform IAM operations.
type IAMOutput struct {
GCP GCPIAMOutput
Azure AzureIAMOutput
AWS AWSIAMOutput
}
// GCPIAMOutput contains the output information of the Terraform IAM operation on GCP.
type GCPIAMOutput struct {
SaKey string
}
// AzureIAMOutput contains the output information of the Terraform IAM operation on Microsoft Azure.
type AzureIAMOutput struct {
SubscriptionID string
TenantID string
UAMIID string
}
// AWSIAMOutput contains the output information of the Terraform IAM operation on GCP.
type AWSIAMOutput struct {
ControlPlaneInstanceProfile string
WorkerNodeInstanceProfile string
}
// ApplyIAMConfig creates an IAM configuration using Terraform.
func (c *Client) ApplyIAMConfig(ctx context.Context, provider cloudprovider.Provider, logLevel LogLevel) (IAMOutput, error) {
if err := c.setLogLevel(logLevel); err != nil {
return IAMOutput{}, fmt.Errorf("set terraform log level %s: %w", logLevel.String(), err)
}
if err := c.tf.Init(ctx); err != nil {
return IAMOutput{}, err
}
if err := c.tf.Apply(ctx); err != nil {
return IAMOutput{}, err
}
return c.ShowIAM(ctx, provider)
}
// Plan determines the diff that will be applied by Terraform. The plan output is written to the planFile.
// If there is a diff, the returned bool is true. Otherwise, it is false.
func (c *Client) Plan(ctx context.Context, logLevel LogLevel, planFile string) (bool, error) {

View file

@ -277,6 +277,9 @@ module "scale_set_group" {
]
}
data "azurerm_subscription" "current" {
}
moved {
from = module.scale_set_control_plane
to = module.scale_set_group["control_plane_default"]

View file

@ -18,3 +18,24 @@ output "initSecret" {
output "attestationURL" {
value = var.create_maa ? azurerm_attestation_provider.attestation_provider[0].attestation_uri : ""
}
output "network_security_group_name" {
value = azurerm_network_security_group.security_group.name
}
output "loadbalancer_name" {
value = azurerm_lb.loadbalancer.name
}
output "user_assigned_identity" {
value = var.user_assigned_identity
}
output "resource_group" {
value = var.resource_group
}
output "subscription_id" {
value = data.azurerm_subscription.current.subscription_id
}

View file

@ -18,3 +18,15 @@ output "initSecret" {
value = random_password.initSecret.result
sensitive = true
}
output "project" {
value = var.project
}
output "ip_cidr_nodes" {
value = local.cidr_vpc_subnet_nodes
}
output "ip_cidr_pods" {
value = local.cidr_vpc_subnet_pods
}

View file

@ -248,6 +248,21 @@ func TestCreateCluster(t *testing.T) {
"api_server_cert_sans": {
Value: []any{"192.0.2.100"},
},
"user_assigned_identity": {
Value: "test_uami_id",
},
"resource_group": {
Value: "test_rg",
},
"subscription_id": {
Value: "test_subscription_id",
},
"network_security_group_name": {
Value: "test_nsg_name",
},
"loadbalancer_name": {
Value: "test_lb_name",
},
},
},
}
@ -435,7 +450,7 @@ func TestCreateCluster(t *testing.T) {
path := path.Join(tc.pathBase, strings.ToLower(tc.provider.String()))
require.NoError(c.PrepareWorkspace(path, tc.vars))
tfOutput, err := c.CreateCluster(context.Background(), LogLevelDebug)
tfOutput, err := c.CreateCluster(context.Background(), tc.provider, LogLevelDebug)
if tc.wantErr {
assert.Error(err)
@ -445,7 +460,9 @@ func TestCreateCluster(t *testing.T) {
assert.Equal("192.0.2.100", tfOutput.IP)
assert.Equal("initSecret", tfOutput.Secret)
assert.Equal("12345abc", tfOutput.UID)
assert.Equal(tc.expectedAttestationURL, tfOutput.AttestationURL)
if tc.provider == cloudprovider.Azure {
assert.Equal(tc.expectedAttestationURL, tfOutput.Azure.AttestationURL)
}
})
}
}