Skip to content

Commit ed26af9

Browse files
committed
** Add the bootstrap ignition component to the capg installation
** create a bucket ** create a signed url ** during a user provisioned dns configuration, edit the ignition data and add the updated/edited data to the ignition data. ** set the url to point to the ignition stub.
1 parent 4fdaf1c commit ed26af9

File tree

5 files changed

+310
-5
lines changed

5 files changed

+310
-5
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package gcp
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"cloud.google.com/go/storage"
9+
"google.golang.org/api/option"
10+
11+
"github.com/openshift/installer/pkg/asset/installconfig"
12+
gcpic "github.com/openshift/installer/pkg/asset/installconfig/gcp"
13+
)
14+
15+
const (
16+
bootstrapIgnitionBucketObjName = "bootstrap.ign"
17+
)
18+
19+
// GetBootstrapStorageName gets the name of the storage bucket for the bootstrap process.
20+
func GetBootstrapStorageName(clusterID string) string {
21+
return fmt.Sprintf("%s-bootstrap-ignition", clusterID)
22+
}
23+
24+
// NewStorageClient creates a new Google storage client.
25+
func NewStorageClient(ctx context.Context) (*storage.Client, error) {
26+
ssn, err := gcpic.GetSession(ctx)
27+
if err != nil {
28+
return nil, fmt.Errorf("failed to get session while creating gcp storage client: %w", err)
29+
}
30+
31+
client, err := storage.NewClient(ctx, option.WithCredentials(ssn.Credentials))
32+
if err != nil {
33+
return nil, fmt.Errorf("failed to create client: %w", err)
34+
}
35+
36+
return client, nil
37+
}
38+
39+
// CreateBucketHandle will create the bucket handle that can be used as a reference for other storage resources.
40+
func CreateBucketHandle(ctx context.Context, bucketName string) (*storage.BucketHandle, error) {
41+
ctx, cancel := context.WithTimeout(ctx, time.Minute*1)
42+
defer cancel()
43+
44+
client, err := NewStorageClient(ctx)
45+
if err != nil {
46+
return nil, fmt.Errorf("failed to create storage client: %w", err)
47+
}
48+
return client.Bucket(bucketName), nil
49+
}
50+
51+
// CreateStorage creates the gcp bucket/storage. The storage bucket does Not include the bucket object. The
52+
// bucket object is created as a separate process/function, so that the two are not tied together, and
53+
// the data stored inside the object can be set at a later time.
54+
func CreateStorage(ctx context.Context, ic *installconfig.InstallConfig, bucketHandle *storage.BucketHandle, clusterID string) error {
55+
labels := map[string]string{}
56+
labels[fmt.Sprintf("kubernetes-io-cluster-%s", clusterID)] = "owned"
57+
for _, label := range ic.Config.GCP.UserLabels {
58+
labels[label.Key] = label.Value
59+
}
60+
61+
bucketAttrs := storage.BucketAttrs{
62+
UniformBucketLevelAccess: storage.UniformBucketLevelAccess{
63+
Enabled: true,
64+
},
65+
Location: ic.Config.GCP.Region,
66+
Labels: labels,
67+
}
68+
69+
ctx, cancel := context.WithTimeout(ctx, time.Minute*1)
70+
defer cancel()
71+
72+
if err := bucketHandle.Create(ctx, ic.Config.GCP.ProjectID, &bucketAttrs); err != nil {
73+
return fmt.Errorf("failed to create bucket: %w", err)
74+
}
75+
return nil
76+
}
77+
78+
// CreateSignedURL creates a signed url and correlates the signed url with a storage bucket.
79+
func CreateSignedURL(handle *storage.BucketHandle, objectName string) (string, error) {
80+
// Signing a URL requires credentials authorized to sign a URL. You can pass
81+
// these in through SignedURLOptions with a Google Access ID with
82+
// iam.serviceAccounts.signBlob permissions.
83+
opts := storage.SignedURLOptions{
84+
Scheme: storage.SigningSchemeV4,
85+
Method: "GET",
86+
Expires: time.Now().Add(time.Minute * 60),
87+
}
88+
89+
// The object has not been created yet. This is ok, it is expected to be created after this call.
90+
// However, if the object is never created this could cause major issues.
91+
url, err := handle.SignedURL(objectName, &opts)
92+
if err != nil {
93+
return "", fmt.Errorf("failed to create a signed url: %w", err)
94+
}
95+
96+
return url, nil
97+
}
98+
99+
// ProvisionBootstrapStorage will provision the required storage bucket and signed url for the bootstrap process.
100+
func ProvisionBootstrapStorage(ctx context.Context, ic *installconfig.InstallConfig, bucketHandle *storage.BucketHandle, clusterID string) (string, error) {
101+
ctx, cancel := context.WithTimeout(ctx, time.Minute*1)
102+
defer cancel()
103+
104+
if err := CreateStorage(ctx, ic, bucketHandle, clusterID); err != nil {
105+
return "", fmt.Errorf("failed to create storage: %w", err)
106+
}
107+
108+
url, err := CreateSignedURL(bucketHandle, bootstrapIgnitionBucketObjName)
109+
if err != nil {
110+
return "", fmt.Errorf("failed to sign url: %w", err)
111+
}
112+
113+
return url, nil
114+
}
115+
116+
// FillBucket will add the contents to the bootstrap storage bucket object.
117+
func FillBucket(ctx context.Context, bucketHandle *storage.BucketHandle, contents string) error {
118+
ctx, cancel := context.WithTimeout(ctx, time.Minute*1)
119+
defer cancel()
120+
121+
objWriter := bucketHandle.Object(bootstrapIgnitionBucketObjName).NewWriter(ctx)
122+
if _, err := fmt.Fprint(objWriter, contents); err != nil {
123+
return fmt.Errorf("failed to store content in bucket object: %w", err)
124+
}
125+
126+
if err := objWriter.Close(); err != nil {
127+
return fmt.Errorf("failed to close bucket object writer: %w", err)
128+
}
129+
130+
return nil
131+
}

pkg/asset/machines/gcp/gcpmachines.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,9 @@ func createCAPIMachine(name string, dataSecret string, infraID string) *capi.Mac
153153
},
154154
Spec: capi.MachineSpec{
155155
ClusterName: infraID,
156-
// Leave empty until ignition support is added
157-
// Bootstrap: capi.Bootstrap{
158-
// DataSecretName: ptr.To(dataSecret),
159-
// },
156+
Bootstrap: capi.Bootstrap{
157+
DataSecretName: ptr.To(dataSecret),
158+
},
160159
InfrastructureRef: v1.ObjectReference{
161160
APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
162161
Kind: "GCPMachine",

pkg/asset/machines/gcp/gcpmachines_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package gcp
33

44
import (
5+
"fmt"
56
"testing"
67

78
"github.com/stretchr/testify/assert"
@@ -206,6 +207,8 @@ func getGCPMachineWithSecureBoot() *capg.GCPMachine {
206207
}
207208

208209
func getBaseCapiMachine() *capi.Machine {
210+
dataSecret := fmt.Sprintf("%s-master", "012345678")
211+
209212
capiMachine := &capi.Machine{
210213
ObjectMeta: metav1.ObjectMeta{
211214
Name: "012345678-master-0",
@@ -215,6 +218,9 @@ func getBaseCapiMachine() *capi.Machine {
215218
},
216219
Spec: capi.MachineSpec{
217220
ClusterName: "012345678",
221+
Bootstrap: capi.Bootstrap{
222+
DataSecretName: ptr.To(dataSecret),
223+
},
218224
InfrastructureRef: v1.ObjectReference{
219225
APIVersion: "infrastructure.cluster.x-k8s.io/v1beta1",
220226
Kind: "GCPMachine",

pkg/infrastructure/gcp/clusterapi/clusterapi.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ package clusterapi
33
import (
44
"context"
55
"fmt"
6+
"time"
67

78
"github.com/sirupsen/logrus"
89
capg "sigs.k8s.io/cluster-api-provider-gcp/api/v1beta1"
910
"sigs.k8s.io/controller-runtime/pkg/client"
1011

12+
"github.com/openshift/installer/pkg/asset/ignition/bootstrap"
13+
"github.com/openshift/installer/pkg/asset/ignition/bootstrap/gcp"
1114
"github.com/openshift/installer/pkg/asset/manifests/capiutils"
1215
"github.com/openshift/installer/pkg/infrastructure/clusterapi"
1316
"github.com/openshift/installer/pkg/types"
@@ -41,7 +44,41 @@ func (p Provider) PreProvision(ctx context.Context, in clusterapi.PreProvisionIn
4144
// added to a bucket. A signed url is generated to point to the bucket and the ignition data will be
4245
// updated to point to the url. This is also allows for bootstrap data to be edited after its initial creation.
4346
func (p Provider) Ignition(ctx context.Context, in clusterapi.IgnitionInput) ([]byte, error) {
44-
return nil, nil
47+
// Create the bucket and presigned url. The url is generated using a known/expected name so that the
48+
// url can be retrieved from the api by this name.
49+
ctx, cancel := context.WithTimeout(ctx, time.Minute*2)
50+
defer cancel()
51+
52+
bucketName := gcp.GetBootstrapStorageName(in.InfraID)
53+
bucketHandle, err := gcp.CreateBucketHandle(ctx, bucketName)
54+
if err != nil {
55+
return nil, fmt.Errorf("failed to create bucket handle %s: %w", bucketName, err)
56+
}
57+
58+
url, err := gcp.ProvisionBootstrapStorage(ctx, in.InstallConfig, bucketHandle, in.InfraID)
59+
if err != nil {
60+
return nil, fmt.Errorf("ignition failed to provision storage: %w", err)
61+
}
62+
editedIgnitionBytes, err := EditIgnition(ctx, in)
63+
if err != nil {
64+
return nil, fmt.Errorf("failed to edit bootstrap ignition: %w", err)
65+
}
66+
67+
ignitionBytes := in.BootstrapIgnData
68+
if editedIgnitionBytes != nil {
69+
ignitionBytes = editedIgnitionBytes
70+
}
71+
72+
if err := gcp.FillBucket(ctx, bucketHandle, string(ignitionBytes)); err != nil {
73+
return nil, fmt.Errorf("ignition failed to fill bucket: %w", err)
74+
}
75+
76+
ignShim, err := bootstrap.GenerateIgnitionShimWithCertBundleAndProxy(url, in.InstallConfig.Config.AdditionalTrustBundle, in.InstallConfig.Config.Proxy)
77+
if err != nil {
78+
return nil, fmt.Errorf("failed to create ignition shim: %w", err)
79+
}
80+
81+
return ignShim, nil
4582
}
4683

4784
// InfraReady is called once cluster.Status.InfrastructureReady
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package clusterapi
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"encoding/json"
7+
"fmt"
8+
"strings"
9+
"time"
10+
11+
igntypes "github.com/coreos/ignition/v2/config/v3_2/types"
12+
capg "sigs.k8s.io/cluster-api-provider-gcp/api/v1beta1"
13+
"sigs.k8s.io/controller-runtime/pkg/client"
14+
"sigs.k8s.io/yaml"
15+
16+
configv1 "github.com/openshift/api/config/v1"
17+
"github.com/openshift/installer/pkg/asset/manifests/capiutils"
18+
"github.com/openshift/installer/pkg/infrastructure/clusterapi"
19+
"github.com/openshift/installer/pkg/types/gcp"
20+
)
21+
22+
const (
23+
infrastructureFilepath = "/opt/openshift/manifests/cluster-infrastructure-02-config.yml"
24+
25+
// replaceable is the string that precedes the encoded data in the ignition data.
26+
// The data must be replaced before decoding the string, and the string must be
27+
// prepended to the encoded data.
28+
replaceable = "data:text/plain;charset=utf-8;base64,"
29+
)
30+
31+
// EditIgnition attempts to edit the contents of the bootstrap ignition when the user has selected
32+
// a custom DNS configuration. Find the public and private load balancer addresses and fill in the
33+
// infrastructure file within the ignition struct.
34+
func EditIgnition(ctx context.Context, in clusterapi.IgnitionInput) ([]byte, error) {
35+
ctx, cancel := context.WithTimeout(ctx, time.Minute*2)
36+
defer cancel()
37+
38+
if in.InstallConfig.Config.GCP.UserProvisionedDNS == gcp.UserProvisionedDNSEnabled {
39+
gcpCluster := &capg.GCPCluster{}
40+
key := client.ObjectKey{
41+
Name: in.InfraID,
42+
Namespace: capiutils.Namespace,
43+
}
44+
if err := in.Client.Get(ctx, key, gcpCluster); err != nil {
45+
return nil, fmt.Errorf("failed to get GCP cluster: %w", err)
46+
}
47+
48+
// public load balancer and health check are created by capi gcp provider.
49+
// TODO: this is currently a global address
50+
apiIPAddress := *gcpCluster.Status.Network.APIServerAddress
51+
52+
ignData := &igntypes.Config{}
53+
err := json.Unmarshal(in.BootstrapIgnData, ignData)
54+
if err != nil {
55+
return nil, fmt.Errorf("failed to unmarshal bootstrap ignition: %w", err)
56+
}
57+
58+
apiIntIPAddress, err := getInternalLBAddress(ctx, in.InstallConfig.Config.GCP.ProjectID, in.InstallConfig.Config.GCP.Region, getAPIAddressName(in.InfraID))
59+
if err != nil {
60+
return nil, fmt.Errorf("failed to create the internal load balancer address: %w", err)
61+
}
62+
63+
err = addLoadBalancersToInfra(gcp.Name, ignData, []string{apiIPAddress}, []string{apiIntIPAddress})
64+
if err != nil {
65+
return nil, fmt.Errorf("failed to add load balancers to ignition config: %w", err)
66+
}
67+
68+
editedIgnBytes, err := json.Marshal(ignData)
69+
if err != nil {
70+
return nil, fmt.Errorf("failed to convert ignition data to json: %w", err)
71+
}
72+
73+
return editedIgnBytes, nil
74+
}
75+
76+
return nil, nil
77+
}
78+
79+
// addLoadBalancersToInfra will load the public and private load balancer information into
80+
// the infrastructure CR. This will occur after the data has already been inserted into the
81+
// ignition file.
82+
func addLoadBalancersToInfra(platform string, config *igntypes.Config, publicLBs []string, privateLBs []string) error {
83+
for i, fileData := range config.Storage.Files {
84+
// update the contents of this file
85+
if fileData.Path == infrastructureFilepath {
86+
contents := config.Storage.Files[i].Contents.Source
87+
replaced := strings.Replace(*contents, replaceable, "", 1)
88+
89+
rawDecodedText, err := base64.StdEncoding.DecodeString(replaced)
90+
if err != nil {
91+
return fmt.Errorf("failed to decode contents of ignition file: %w", err)
92+
}
93+
94+
infra := &configv1.Infrastructure{}
95+
if err := yaml.Unmarshal(rawDecodedText, infra); err != nil {
96+
return fmt.Errorf("failed to unmarshal infrastructure: %w", err)
97+
}
98+
99+
// convert the list of strings to a list of IPs
100+
apiIntLbs := []configv1.IP{}
101+
for _, ip := range privateLBs {
102+
apiIntLbs = append(apiIntLbs, configv1.IP(ip))
103+
}
104+
apiLbs := []configv1.IP{}
105+
for _, ip := range publicLBs {
106+
apiLbs = append(apiLbs, configv1.IP(ip))
107+
}
108+
cloudLBInfo := configv1.CloudLoadBalancerIPs{
109+
APIIntLoadBalancerIPs: apiIntLbs,
110+
APILoadBalancerIPs: apiLbs,
111+
}
112+
113+
if infra.Status.PlatformStatus.GCP.CloudLoadBalancerConfig.DNSType == configv1.ClusterHostedDNSType {
114+
infra.Status.PlatformStatus.GCP.CloudLoadBalancerConfig.ClusterHosted = &cloudLBInfo
115+
}
116+
117+
// convert the infrastructure back to an encoded string
118+
infraContents, err := yaml.Marshal(infra)
119+
if err != nil {
120+
return fmt.Errorf("failed to marshal infrastructure: %w", err)
121+
}
122+
123+
encoded := fmt.Sprintf("%s%s", replaceable, base64.StdEncoding.EncodeToString(infraContents))
124+
// replace the contents with the edited information
125+
config.Storage.Files[i].Contents.Source = &encoded
126+
127+
break
128+
}
129+
}
130+
131+
return nil
132+
}

0 commit comments

Comments
 (0)