Skip to content

Commit 7645fef

Browse files
Merge pull request #8014 from barbacbd/CORS-3259
CORS-3259: GCP: Create DNS records and internal load balancer for CAPG Install
2 parents 545a7aa + f1ffd56 commit 7645fef

File tree

7 files changed

+408
-0
lines changed

7 files changed

+408
-0
lines changed

pkg/infrastructure/gcp/clusterapi/clusterapi.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,15 @@ package clusterapi
22

33
import (
44
"context"
5+
"fmt"
56

7+
"github.com/sirupsen/logrus"
8+
capg "sigs.k8s.io/cluster-api-provider-gcp/api/v1beta1"
9+
"sigs.k8s.io/controller-runtime/pkg/client"
10+
11+
"github.com/openshift/installer/pkg/asset/manifests/capiutils"
612
"github.com/openshift/installer/pkg/infrastructure/clusterapi"
13+
"github.com/openshift/installer/pkg/types"
714
gcptypes "github.com/openshift/installer/pkg/types/gcp"
815
)
916

@@ -41,6 +48,50 @@ func (p Provider) Ignition(ctx context.Context, in clusterapi.IgnitionInput) ([]
4148
// is true, typically after load balancers have been provisioned. It can be used
4249
// to create DNS records.
4350
func (p Provider) InfraReady(ctx context.Context, in clusterapi.InfraReadyInput) error {
51+
gcpCluster := &capg.GCPCluster{}
52+
key := client.ObjectKey{
53+
Name: in.InfraID,
54+
Namespace: capiutils.Namespace,
55+
}
56+
if err := in.Client.Get(ctx, key, gcpCluster); err != nil {
57+
return fmt.Errorf("failed to get GCP cluster: %w", err)
58+
}
59+
60+
// public load balancer is created by CAPG. The health check for this load balancer is also created by
61+
// the CAPG.
62+
apiIPAddress := gcpCluster.Spec.ControlPlaneEndpoint.Host
63+
if apiIPAddress == "" && in.InstallConfig.Config.Publish == types.ExternalPublishingStrategy {
64+
logrus.Debugf("publish strategy is set to external but api address is empty")
65+
}
66+
67+
// Currently, the internal/private load balancer is not created by CAPG. The load balancer will be created
68+
// by the installer for now
69+
// TODO: remove the creation of the LB and health check here when supported by CAPG.
70+
// https://github.com/kubernetes-sigs/cluster-api-provider-gcp/issues/903
71+
// Create the public (optional) and private load balancer static ip addresses
72+
// TODO: Do we then need to setup a subnet for internal load balancing ?
73+
apiIntIPAddress, err := createInternalLBAddress(ctx, in)
74+
if err != nil {
75+
return fmt.Errorf("failed to create internal load balancer address: %w", err)
76+
}
77+
78+
if in.InstallConfig.Config.GCP.UserProvisionedDNS != gcptypes.UserProvisionedDNSEnabled {
79+
// Get the network from the GCP Cluster. The network is used to create the private managed zone.
80+
if gcpCluster.Status.Network.SelfLink == nil {
81+
return fmt.Errorf("failed to get GCP network: %w", err)
82+
}
83+
84+
// Create the private zone if one does not exist
85+
if err := createPrivateManagedZone(ctx, in.InstallConfig, in.InfraID, *gcpCluster.Status.Network.SelfLink); err != nil {
86+
return fmt.Errorf("failed to create the private managed zone: %w", err)
87+
}
88+
89+
// Create the public (optional) and private dns records
90+
if err := createDNSRecords(ctx, in.InstallConfig, in.InfraID, apiIPAddress, apiIntIPAddress); err != nil {
91+
return fmt.Errorf("failed to create DNS records: %w", err)
92+
}
93+
}
94+
4495
return nil
4596
}
4697

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package clusterapi
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"time"
8+
9+
"google.golang.org/api/dns/v1"
10+
11+
"github.com/openshift/installer/pkg/asset/installconfig"
12+
gcpic "github.com/openshift/installer/pkg/asset/installconfig/gcp"
13+
"github.com/openshift/installer/pkg/types"
14+
)
15+
16+
var (
17+
errNotFound = errors.New("not found")
18+
)
19+
20+
func getDNSZoneName(ctx context.Context, ic *installconfig.InstallConfig, isPublic bool) (string, error) {
21+
ctx, cancel := context.WithTimeout(ctx, time.Minute*1)
22+
defer cancel()
23+
24+
client, err := gcpic.NewClient(ctx)
25+
if err != nil {
26+
return "", fmt.Errorf("failed to create new client: %w", err)
27+
}
28+
29+
cctx, ccancel := context.WithTimeout(ctx, time.Minute*1)
30+
defer ccancel()
31+
32+
domain := ic.Config.ClusterDomain()
33+
if isPublic {
34+
domain = ic.Config.BaseDomain
35+
}
36+
37+
zone, err := client.GetDNSZone(cctx, ic.Config.GCP.ProjectID, domain, isPublic)
38+
if err != nil {
39+
return "", fmt.Errorf("failed to get dns zone name: %w", err)
40+
}
41+
42+
if zone != nil {
43+
return zone.Name, nil
44+
}
45+
46+
return "", errNotFound
47+
}
48+
49+
type recordSet struct {
50+
projectID string
51+
zoneName string
52+
record *dns.ResourceRecordSet
53+
}
54+
55+
// createRecordSets will create a list of records that will be created during the install.
56+
func createRecordSets(ctx context.Context, ic *installconfig.InstallConfig, clusterID, apiIP, apiIntIP string) ([]recordSet, error) {
57+
ctx, cancel := context.WithTimeout(ctx, time.Minute*1)
58+
defer cancel()
59+
60+
// A shared VPC install allows a user to preconfigure a private zone. If there is a private zone found,
61+
// use the existing private zone otherwise default to the installer private zone pattern below.
62+
privateZoneName, err := getDNSZoneName(ctx, ic, false)
63+
if err != nil {
64+
if !errors.Is(err, errNotFound) {
65+
return nil, fmt.Errorf("failed to find private zone: %w", err)
66+
}
67+
privateZoneName = fmt.Sprintf("%s-private-zone", clusterID)
68+
}
69+
70+
records := []recordSet{
71+
{
72+
// api_internal
73+
projectID: ic.Config.GCP.ProjectID,
74+
zoneName: privateZoneName,
75+
record: &dns.ResourceRecordSet{
76+
Name: fmt.Sprintf("api-int.%s.", ic.Config.ClusterDomain()),
77+
Type: "A",
78+
Ttl: 60,
79+
Rrdatas: []string{apiIntIP},
80+
},
81+
},
82+
{
83+
// api_external_internal_zone
84+
projectID: ic.Config.GCP.ProjectID,
85+
zoneName: privateZoneName,
86+
record: &dns.ResourceRecordSet{
87+
Name: fmt.Sprintf("api.%s.", ic.Config.ClusterDomain()),
88+
Type: "A",
89+
Ttl: 60,
90+
Rrdatas: []string{apiIntIP},
91+
},
92+
},
93+
}
94+
95+
if ic.Config.Publish == types.ExternalPublishingStrategy {
96+
existingPublicZoneName, err := getDNSZoneName(ctx, ic, true)
97+
if err != nil {
98+
return nil, fmt.Errorf("failed to find a public zone: %w", err)
99+
}
100+
101+
apiRecord := recordSet{
102+
projectID: ic.Config.GCP.ProjectID,
103+
zoneName: existingPublicZoneName,
104+
record: &dns.ResourceRecordSet{
105+
Name: fmt.Sprintf("api.%s.", ic.Config.ClusterDomain()),
106+
Type: "A",
107+
Ttl: 60,
108+
Rrdatas: []string{apiIP},
109+
},
110+
}
111+
records = append(records, apiRecord)
112+
}
113+
114+
return records, nil
115+
}
116+
117+
// createDNSRecords will get the list of records to be created and execute their creation through the gcp dns api.
118+
func createDNSRecords(ctx context.Context, ic *installconfig.InstallConfig, clusterID, apiIP, apiIntIP string) error {
119+
// TODO: use the opts for the service to restrict scopes see google.golang.org/api/option.WithScopes
120+
dnsService, err := dns.NewService(ctx)
121+
if err != nil {
122+
return fmt.Errorf("failed to create the gcp dns service: %w", err)
123+
}
124+
125+
records, err := createRecordSets(ctx, ic, clusterID, apiIP, apiIntIP)
126+
if err != nil {
127+
return err
128+
}
129+
130+
// 1 minute timeout for each record
131+
ctx, cancel := context.WithTimeout(ctx, time.Minute*time.Duration(len(records)))
132+
defer cancel()
133+
134+
for _, record := range records {
135+
if _, err := dnsService.ResourceRecordSets.Create(record.projectID, record.zoneName, record.record).Context(ctx).Do(); err != nil {
136+
return fmt.Errorf("failed to create record set %s: %w", record.record.Name, err)
137+
}
138+
}
139+
140+
return nil
141+
}
142+
143+
// createPrivateManagedZone will create a private managed zone in the GCP project specified in the install config. The
144+
// private managed zone should only be created when one is not specified in the install config.
145+
func createPrivateManagedZone(ctx context.Context, ic *installconfig.InstallConfig, clusterID, network string) error {
146+
// TODO: use the opts for the service to restrict scopes see google.golang.org/api/option.WithScopes
147+
dnsService, err := dns.NewService(ctx)
148+
if err != nil {
149+
return fmt.Errorf("failed to create the gcp dns service: %w", err)
150+
}
151+
152+
managedZone := &dns.ManagedZone{
153+
Name: fmt.Sprintf("%s-private-zone", clusterID),
154+
Description: resourceDescription,
155+
DnsName: fmt.Sprintf("%s.", ic.Config.ClusterDomain()),
156+
Visibility: "private",
157+
Labels: mergeLabels(ic, clusterID),
158+
PrivateVisibilityConfig: &dns.ManagedZonePrivateVisibilityConfig{
159+
Networks: []*dns.ManagedZonePrivateVisibilityConfigNetwork{
160+
{
161+
NetworkUrl: network,
162+
},
163+
},
164+
},
165+
}
166+
167+
ctx, cancel := context.WithTimeout(ctx, time.Minute*1)
168+
defer cancel()
169+
170+
if _, err = dnsService.ManagedZones.Create(ic.Config.GCP.ProjectID, managedZone).Context(ctx).Do(); err != nil {
171+
return fmt.Errorf("failed to create private managed zone: %w", err)
172+
}
173+
174+
return nil
175+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package clusterapi
2+
3+
const (
4+
resourceDescription = "Created By OpenShift Installer"
5+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package clusterapi
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/openshift/installer/pkg/asset/installconfig"
7+
)
8+
9+
func mergeLabels(ic *installconfig.InstallConfig, clusterID string) map[string]string {
10+
labels := map[string]string{}
11+
labels[fmt.Sprintf("kubernetes-io-cluster-%s", clusterID)] = "owned"
12+
for _, label := range ic.Config.GCP.UserLabels {
13+
labels[label.Key] = label.Value
14+
}
15+
16+
return labels
17+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package clusterapi
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"google.golang.org/api/compute/v1"
9+
10+
"github.com/openshift/installer/pkg/infrastructure/clusterapi"
11+
)
12+
13+
func getAPIInternalResourceName(infraID string) string {
14+
return fmt.Sprintf("%s-api-internal", infraID)
15+
}
16+
17+
func getAPIAddressName(infraID string) string {
18+
return fmt.Sprintf("%s-cluster-ip", infraID)
19+
}
20+
21+
func getInternalLBAddress(ctx context.Context, project, region, name string) (string, error) {
22+
service, err := NewComputeService()
23+
if err != nil {
24+
return "", err
25+
}
26+
27+
addrOutput, err := service.Addresses.Get(project, region, name).Context(ctx).Do()
28+
if err != nil {
29+
return "", fmt.Errorf("failed to get compute address %s: %w", name, err)
30+
}
31+
return addrOutput.Address, nil
32+
}
33+
34+
// createInternalLBAddress creates a static ip address for the internal load balancer.
35+
func createInternalLBAddress(ctx context.Context, in clusterapi.InfraReadyInput) (string, error) {
36+
name := getAPIAddressName(in.InfraID)
37+
38+
// TODO: Find the self link for a subnet from the in.Client
39+
// TODO: can we pick one returned from the service.Get() ?
40+
subnetSelfLink := ""
41+
42+
// TODO: the subnet is only relevant for internal load balancer
43+
addr := &compute.Address{
44+
Name: name,
45+
AddressType: "INTERNAL",
46+
Subnetwork: subnetSelfLink,
47+
Description: resourceDescription,
48+
Labels: mergeLabels(in.InstallConfig, in.InfraID),
49+
Region: in.InstallConfig.Config.GCP.Region,
50+
}
51+
52+
service, err := NewComputeService()
53+
if err != nil {
54+
return "", err
55+
}
56+
57+
ctx, cancel := context.WithTimeout(ctx, time.Minute*1)
58+
defer cancel()
59+
60+
op, err := service.Addresses.Insert(in.InstallConfig.Config.GCP.ProjectID, in.InstallConfig.Config.GCP.Region, addr).Context(ctx).Do()
61+
if err != nil {
62+
return "", fmt.Errorf("failed to create internal compute address: %w", err)
63+
}
64+
65+
if err := WaitForOperationRegional(ctx, in.InstallConfig.Config.GCP.ProjectID, in.InstallConfig.Config.GCP.Region, op); err != nil {
66+
return "", fmt.Errorf("failed to wait for compute address creation: %w", err)
67+
}
68+
69+
ipAddress, err := getInternalLBAddress(ctx, in.InstallConfig.Config.GCP.ProjectID, in.InstallConfig.Config.GCP.Region, name)
70+
if err != nil {
71+
return "", fmt.Errorf("failed to get internal load balancer IP address: %w", err)
72+
}
73+
74+
healthCheck := &compute.HealthCheck{
75+
Name: getAPIInternalResourceName(in.InfraID),
76+
Description: resourceDescription,
77+
HealthyThreshold: 3,
78+
UnhealthyThreshold: 3,
79+
CheckIntervalSec: 2,
80+
TimeoutSec: 2,
81+
Type: "HTTPS",
82+
HttpsHealthCheck: &compute.HTTPSHealthCheck{
83+
Port: 6443,
84+
RequestPath: "/readyz",
85+
},
86+
}
87+
88+
if _, err := service.HealthChecks.Insert(in.InstallConfig.Config.GCP.ProjectID, healthCheck).Context(ctx).Do(); err != nil {
89+
return "", fmt.Errorf("failed to create api-internal health check: %w", err)
90+
}
91+
92+
return ipAddress, nil
93+
}

0 commit comments

Comments
 (0)