Skip to content

Commit 36bbe11

Browse files
Merge pull request openshift#8669 from jhixson74/capz_bootstrap_ssh
CORS-3302: port forward SSH to bootstrap host
2 parents f6df3a3 + 80dd6b5 commit 36bbe11

File tree

6 files changed

+327
-51
lines changed

6 files changed

+327
-51
lines changed

pkg/asset/installconfig/azure/session.go

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -83,24 +83,9 @@ func GetSessionWithCredentials(cloudName azure.CloudEnvironment, armEndpoint str
8383
return nil, fmt.Errorf("failed to get Azure environment for the %q cloud: %w", cloudName, err)
8484
}
8585

86-
var cloudConfig cloud.Configuration
87-
switch cloudName {
88-
case azure.StackCloud:
89-
cloudConfig = cloud.Configuration{
90-
ActiveDirectoryAuthorityHost: cloudEnv.ActiveDirectoryEndpoint,
91-
Services: map[cloud.ServiceName]cloud.ServiceConfiguration{
92-
cloud.ResourceManager: {
93-
Audience: cloudEnv.TokenAudience,
94-
Endpoint: cloudEnv.ResourceManagerEndpoint,
95-
},
96-
},
97-
}
98-
case azure.USGovernmentCloud:
99-
cloudConfig = cloud.AzureGovernment
100-
case azure.ChinaCloud:
101-
cloudConfig = cloud.AzureChina
102-
default:
103-
cloudConfig = cloud.AzurePublic
86+
cloudConfig, err := GetCloudConfiguration(cloudName, armEndpoint)
87+
if err != nil {
88+
return nil, fmt.Errorf("failed to get cloud configuration for the %q cloud: %w", cloudName, err)
10489
}
10590

10691
if credentials == nil {
@@ -114,13 +99,13 @@ func GetSessionWithCredentials(cloudName azure.CloudEnvironment, armEndpoint str
11499
switch {
115100
case credentials.ClientCertificatePath != "":
116101
logrus.Warnf("Using client certs to authenticate. Please be warned cluster does not support certs and only the installer does.")
117-
cred, err = newTokenCredentialFromCertificates(credentials, cloudConfig)
102+
cred, err = newTokenCredentialFromCertificates(credentials, *cloudConfig)
118103
authType = ClientCertificateAuth
119104
case credentials.ClientSecret != "":
120-
cred, err = newTokenCredentialFromCredentials(credentials, cloudConfig)
105+
cred, err = newTokenCredentialFromCredentials(credentials, *cloudConfig)
121106
authType = ClientSecretAuth
122107
default:
123-
cred, err = newTokenCredentialFromMSI(credentials, cloudConfig)
108+
cred, err = newTokenCredentialFromMSI(credentials, *cloudConfig)
124109
authType = ManagedIdentityAuth
125110
}
126111
if err != nil {
@@ -130,11 +115,48 @@ func GetSessionWithCredentials(cloudName azure.CloudEnvironment, armEndpoint str
130115
if err != nil {
131116
return nil, err
132117
}
133-
session.CloudConfig = cloudConfig
118+
session.CloudConfig = *cloudConfig
134119
session.AuthType = authType
135120
return session, nil
136121
}
137122

123+
// GetCloudConfiguration gets a cloud configuration from the cloud name and endpoint.
124+
func GetCloudConfiguration(cloudName azure.CloudEnvironment, armEndpoint string) (*cloud.Configuration, error) {
125+
var cloudEnv azureenv.Environment
126+
var err error
127+
switch cloudName {
128+
case azure.StackCloud:
129+
cloudEnv, err = azureenv.EnvironmentFromURL(armEndpoint)
130+
default:
131+
cloudEnv, err = azureenv.EnvironmentFromName(string(cloudName))
132+
}
133+
if err != nil {
134+
return nil, err
135+
}
136+
137+
var cloudConfig cloud.Configuration
138+
switch cloudName {
139+
case azure.StackCloud:
140+
cloudConfig = cloud.Configuration{
141+
ActiveDirectoryAuthorityHost: cloudEnv.ActiveDirectoryEndpoint,
142+
Services: map[cloud.ServiceName]cloud.ServiceConfiguration{
143+
cloud.ResourceManager: {
144+
Audience: cloudEnv.TokenAudience,
145+
Endpoint: cloudEnv.ResourceManagerEndpoint,
146+
},
147+
},
148+
}
149+
case azure.USGovernmentCloud:
150+
cloudConfig = cloud.AzureGovernment
151+
case azure.ChinaCloud:
152+
cloudConfig = cloud.AzureChina
153+
default:
154+
cloudConfig = cloud.AzurePublic
155+
}
156+
157+
return &cloudConfig, nil
158+
}
159+
138160
// credentialsFromFileOrUser returns credentials found
139161
// in ~/.azure/osServicePrincipal.json and, if no creds are found,
140162
// asks for them and stores them on disk in a config file

pkg/asset/machines/azure/azuremachines.go

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -201,16 +201,10 @@ func GenerateMachines(platform *azure.Platform, pool *types.MachinePool, userDat
201201
OSDisk: osDisk,
202202
AdditionalTags: tags,
203203
DisableExtensionOperations: ptr.To(true),
204-
// Do not allocate a public IP since it isn't
205-
// accessible as we are using an outbound LB for the
206-
// control plane. This is temporary until we have a
207-
// workaround for accessing SSH (Most likely port
208-
// forwarding SSH off the LB until the bootstrap node
209-
// is destroyed).
210-
AllocatePublicIP: false,
211-
AdditionalCapabilities: additionalCapabilities,
212-
SecurityProfile: securityProfile,
213-
Identity: capz.VMIdentityUserAssigned,
204+
AllocatePublicIP: true,
205+
AdditionalCapabilities: additionalCapabilities,
206+
SecurityProfile: securityProfile,
207+
Identity: capz.VMIdentityUserAssigned,
214208
UserAssignedIdentities: []capz.UserAssignedIdentity{
215209
{
216210
ProviderID: userAssignedIdentityID,

pkg/asset/machines/azure/machines.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ func provider(platform *azure.Platform, mpool *azure.MachinePool, osImage string
184184
image.Version = mpool.OSImage.Version
185185
} else if useImageGallery {
186186
// image gallery names cannot have dashes
187-
galleryName := strings.Replace(clusterID, "-", "_", -1)
187+
galleryName := strings.ReplaceAll(clusterID, "-", "_")
188188
id := clusterID
189189
if hyperVGen == "V2" {
190190
id += "-gen2"

pkg/infrastructure/azure/azure.go

Lines changed: 112 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
"strings"
1010
"time"
1111

12+
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
1213
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
14+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
1315
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
1416
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3"
1517
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4"
@@ -25,6 +27,7 @@ import (
2527
"sigs.k8s.io/controller-runtime/pkg/client"
2628

2729
"github.com/openshift/installer/pkg/asset/ignition/bootstrap"
30+
azconfig "github.com/openshift/installer/pkg/asset/installconfig/azure"
2831
"github.com/openshift/installer/pkg/asset/manifests/capiutils"
2932
"github.com/openshift/installer/pkg/infrastructure/clusterapi"
3033
"github.com/openshift/installer/pkg/rhcos"
@@ -45,14 +48,18 @@ type Provider struct {
4548
StorageAccount *armstorage.Account
4649
StorageClientFactory *armstorage.ClientFactory
4750
StorageAccountKeys []armstorage.AccountKey
48-
Tags map[string]*string
51+
NetworkClientFactory *armnetwork.ClientFactory
4952
lbBackendAddressPool *armnetwork.BackendAddressPool
53+
CloudConfiguration cloud.Configuration
54+
TokenCredential azcore.TokenCredential
55+
Tags map[string]*string
5056
}
5157

5258
var _ clusterapi.PreProvider = (*Provider)(nil)
5359
var _ clusterapi.InfraReadyProvider = (*Provider)(nil)
5460
var _ clusterapi.PostProvider = (*Provider)(nil)
5561
var _ clusterapi.IgnitionProvider = (*Provider)(nil)
62+
var _ clusterapi.PostDestroyer = (*Provider)(nil)
5663

5764
// Name returns the name of the provider.
5865
func (p *Provider) Name() string {
@@ -239,7 +246,7 @@ func (p *Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput
239246
cloudConfiguration := session.CloudConfig
240247

241248
resourceGroupName := p.ResourceGroupName
242-
storageAccountName := fmt.Sprintf("cluster%s", randomString(5))
249+
storageAccountName := fmt.Sprintf("%ssa", strings.ReplaceAll(in.InfraID, "-", ""))
243250
containerName := "vhd"
244251
blobName := fmt.Sprintf("rhcos%s.vhd", randomString(5))
245252

@@ -469,7 +476,7 @@ func (p *Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput
469476
}
470477
logrus.Debugf("created public ip: %s", *publicIP.ID)
471478

472-
loadBalancer, err := updateExternalLoadBalancer(ctx, publicIP, lbInput)
479+
loadBalancer, err := updateOutboundLoadBalancerToAPILoadBalancer(ctx, publicIP, lbInput)
473480
if err != nil {
474481
return fmt.Errorf("failed to update external load balancer: %w", err)
475482
}
@@ -484,8 +491,9 @@ func (p *Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput
484491
p.StorageAccountName = storageAccountName
485492
p.StorageURL = storageURL
486493
p.StorageAccount = storageAccount
487-
p.StorageClientFactory = storageClientFactory
488494
p.StorageAccountKeys = storageAccountKeys
495+
p.StorageClientFactory = storageClientFactory
496+
p.NetworkClientFactory = networkClientFactory
489497
p.lbBackendAddressPool = lbBap
490498

491499
if err := createDNSEntries(ctx, in, extLBFQDN, resourceGroupName); err != nil {
@@ -516,16 +524,6 @@ func (p *Provider) PostProvision(ctx context.Context, in clusterapi.PostProvisio
516524
if err != nil {
517525
return fmt.Errorf("error creating vm client: %w", err)
518526
}
519-
nicClient, err := armnetwork.NewInterfacesClient(ssn.Credentials.SubscriptionID, ssn.TokenCreds,
520-
&arm.ClientOptions{
521-
ClientOptions: policy.ClientOptions{
522-
Cloud: cloudConfiguration,
523-
},
524-
},
525-
)
526-
if err != nil {
527-
return fmt.Errorf("error creating nic client: %w", err)
528-
}
529527

530528
vmIDs, err := getControlPlaneIDs(in.Client, in.InstallConfig.Config.ControlPlane.Replicas, in.InfraID)
531529
if err != nil {
@@ -536,14 +534,113 @@ func (p *Provider) PostProvision(ctx context.Context, in clusterapi.PostProvisio
536534
infraID: in.InfraID,
537535
resourceGroup: p.ResourceGroupName,
538536
vmClient: vmClient,
539-
nicClient: nicClient,
537+
nicClient: p.NetworkClientFactory.NewInterfacesClient(),
540538
ids: vmIDs,
541539
bap: p.lbBackendAddressPool,
542540
}
543541

544542
if err = associateVMToBackendPool(ctx, *vmInput); err != nil {
545543
return fmt.Errorf("failed to associate control plane VMs with external load balancer: %w", err)
546544
}
545+
546+
if err = addSecurityGroupRule(ctx, &securityGroupInput{
547+
resourceGroupName: p.ResourceGroupName,
548+
securityGroupName: fmt.Sprintf("%s-nsg", in.InfraID),
549+
securityRuleName: "ssh_in",
550+
securityRulePort: "22",
551+
securityRulePriority: 220,
552+
networkClientFactory: p.NetworkClientFactory,
553+
}); err != nil {
554+
return fmt.Errorf("failed to add security rule: %w", err)
555+
}
556+
557+
loadBalancerName := in.InfraID
558+
frontendIPConfigName := "public-lb-ip-v4"
559+
frontendIPConfigID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/frontendIPConfigurations/%s",
560+
subscriptionID,
561+
p.ResourceGroupName,
562+
loadBalancerName,
563+
frontendIPConfigName,
564+
)
565+
566+
// Create an inbound nat rule that forwards port 22 on the
567+
// public load balancer to the bootstrap host. This takes 2
568+
// stages to accomplish. First, the nat rule needs to be added
569+
// to the frontend IP configuration on the public load
570+
// balancer. Second, the nat rule needs to be addded to the
571+
// bootstrap interface with the association to the rule on the
572+
// public load balancer.
573+
inboundNatRule, err := addInboundNatRuleToLoadBalancer(ctx, &inboundNatRuleInput{
574+
resourceGroupName: p.ResourceGroupName,
575+
loadBalancerName: loadBalancerName,
576+
frontendIPConfigID: frontendIPConfigID,
577+
inboundNatRuleName: "ssh_in",
578+
inboundNatRulePort: 22,
579+
networkClientFactory: p.NetworkClientFactory,
580+
})
581+
if err != nil {
582+
return fmt.Errorf("failed to create inbound nat rule: %w", err)
583+
}
584+
_, err = associateInboundNatRuleToInterface(ctx, &inboundNatRuleInput{
585+
resourceGroupName: p.ResourceGroupName,
586+
loadBalancerName: loadBalancerName,
587+
bootstrapNicName: fmt.Sprintf("%s-bootstrap-nic", in.InfraID),
588+
frontendIPConfigID: frontendIPConfigID,
589+
inboundNatRuleID: *inboundNatRule.ID,
590+
inboundNatRuleName: "ssh_in",
591+
inboundNatRulePort: 22,
592+
networkClientFactory: p.NetworkClientFactory,
593+
})
594+
if err != nil {
595+
return fmt.Errorf("failed to associate inbound nat rule to interface: %w", err)
596+
}
597+
}
598+
599+
return nil
600+
}
601+
602+
// PostDestroy removes SSH access from the network security rules and removes
603+
// SSH port forwarding off the public load balancer when the bootstrap machine
604+
// is destroyed.
605+
func (p *Provider) PostDestroy(ctx context.Context, in clusterapi.PostDestroyerInput) error {
606+
session, err := azconfig.GetSession(in.Metadata.Azure.CloudName, in.Metadata.Azure.ARMEndpoint)
607+
if err != nil {
608+
return fmt.Errorf("failed to get session: %w", err)
609+
}
610+
611+
networkClientFactory, err := armnetwork.NewClientFactory(
612+
session.Credentials.SubscriptionID,
613+
session.TokenCreds,
614+
&arm.ClientOptions{
615+
ClientOptions: policy.ClientOptions{
616+
Cloud: session.CloudConfig,
617+
},
618+
},
619+
)
620+
if err != nil {
621+
return fmt.Errorf("error creating network client factory: %w", err)
622+
}
623+
624+
// XXX: why is in.Metadata.Azure.ResourceGroupName empty?
625+
err = deleteSecurityGroupRule(ctx, &securityGroupInput{
626+
resourceGroupName: fmt.Sprintf("%s-rg", in.Metadata.InfraID),
627+
securityGroupName: fmt.Sprintf("%s-nsg", in.Metadata.InfraID),
628+
securityRuleName: "ssh_in",
629+
securityRulePort: "22",
630+
networkClientFactory: networkClientFactory,
631+
})
632+
if err != nil {
633+
return fmt.Errorf("failed to delete security rule: %w", err)
634+
}
635+
636+
err = deleteInboundNatRule(ctx, &inboundNatRuleInput{
637+
resourceGroupName: fmt.Sprintf("%s-rg", in.Metadata.InfraID),
638+
loadBalancerName: in.Metadata.InfraID,
639+
inboundNatRuleName: "ssh_in",
640+
networkClientFactory: networkClientFactory,
641+
})
642+
if err != nil {
643+
return fmt.Errorf("failed to delete inbound nat rule: %w", err)
547644
}
548645

549646
return nil

0 commit comments

Comments
 (0)