github.com/openshift/installer@v1.4.17/pkg/infrastructure/openstack/preprovision/bootstrapignition.go (about) 1 package preprovision 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/base64" 7 "encoding/json" 8 "encoding/pem" 9 "fmt" 10 "os" 11 "strings" 12 13 igntypes "github.com/coreos/ignition/v2/config/v3_2/types" 14 "github.com/gophercloud/gophercloud/v2" 15 gophercloud_openstack "github.com/gophercloud/gophercloud/v2/openstack" 16 "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" 17 "github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata" 18 "github.com/gophercloud/gophercloud/v2/openstack/image/v2/images" 19 "github.com/gophercloud/utils/v2/openstack/clientconfig" 20 "github.com/sirupsen/logrus" 21 "github.com/vincent-petithory/dataurl" 22 "k8s.io/utils/ptr" 23 24 "github.com/openshift/installer/pkg/asset" 25 "github.com/openshift/installer/pkg/asset/ignition" 26 "github.com/openshift/installer/pkg/asset/installconfig" 27 "github.com/openshift/installer/pkg/types" 28 openstackdefaults "github.com/openshift/installer/pkg/types/openstack/defaults" 29 ) 30 31 // ReplaceBootstrapIgnitionInTFVars replaces the ignition file in the terraform variables. 32 func ReplaceBootstrapIgnitionInTFVars(ctx context.Context, tfvarsFile *asset.File, installConfig *installconfig.InstallConfig, clusterID *installconfig.ClusterID) error { 33 if tfvarsFile == nil { 34 return fmt.Errorf("missing tfvars file") 35 } 36 37 var tfvars map[string]json.RawMessage 38 if err := json.Unmarshal(tfvarsFile.Data, &tfvars); err != nil { 39 return fmt.Errorf("unable to decode tfvars: %w", err) 40 } 41 42 bootstrapIgnitionJSON, ok := tfvars["openstack_bootstrap_shim_ignition"] 43 if !ok { 44 return fmt.Errorf("bootstrap's Ignition file not found") 45 } 46 47 var bootstrapIgnition string 48 if err := json.Unmarshal(bootstrapIgnitionJSON, &bootstrapIgnition); err != nil { 49 return fmt.Errorf("failed to decode bootstrap's Ignition") 50 } 51 52 logrus.Debugf("Uploading Ignition to Glance") 53 bootstrapShim, err := UploadIgnitionAndBuildShim(ctx, installConfig.Config.Platform.OpenStack.Cloud, clusterID.InfraID, installConfig.Config.Proxy, []byte(bootstrapIgnition)) 54 if err != nil { 55 return fmt.Errorf("failed to build bootstrap's Ignition shim: %w", err) 56 } 57 58 bootstrapShimJSON, err := json.Marshal(bootstrapShim) 59 if err != nil { 60 return fmt.Errorf("failed to encode bootstrap's Ignition shim: %w", err) 61 } 62 63 logrus.Debugf("Replacing the Ignition file in the Terraform variables") 64 tfvars["openstack_bootstrap_shim_ignition"] = bootstrapShimJSON 65 66 b, err := json.MarshalIndent(tfvars, "", " ") 67 if err != nil { 68 return fmt.Errorf("unable to encode tfvars: %w", err) 69 } 70 tfvarsFile.Data = b 71 return nil 72 } 73 74 // UploadIgnitionAndBuildShim uploads the bootstrap Ignition config in Glance. 75 func UploadIgnitionAndBuildShim(ctx context.Context, cloud string, infraID string, proxy *types.Proxy, bootstrapIgn []byte) ([]byte, error) { 76 opts := openstackdefaults.DefaultClientOpts(cloud) 77 conn, err := openstackdefaults.NewServiceClient(ctx, "image", opts) 78 if err != nil { 79 return nil, err 80 } 81 82 var userCA []byte 83 { 84 cloudConfig, err := clientconfig.GetCloudFromYAML(opts) 85 if err != nil { 86 return nil, err 87 } 88 // Get the ca-cert-bundle key if there is a value for cacert in clouds.yaml 89 if caPath := cloudConfig.CACertFile; caPath != "" { 90 userCA, err = os.ReadFile(caPath) 91 if err != nil { 92 return nil, fmt.Errorf("failed to read clouds.yaml ca-cert from disk: %w", err) 93 } 94 } 95 } 96 97 // we need to obtain Glance public endpoint that will be used by Ignition to download bootstrap ignition files. 98 // By design this should be done by using https://www.terraform.io/docs/providers/openstack/d/identity_endpoint_v3.html 99 // but OpenStack default policies forbid to use this API for regular users. 100 // On the other hand when a user authenticates in OpenStack (i.e. gets a token), it includes the whole service 101 // catalog in the output json. So we are able to parse the data and get the endpoint from there 102 // https://docs.openstack.org/api-ref/identity/v3/?expanded=token-authentication-with-scoped-authorization-detail#token-authentication-with-scoped-authorization 103 // Unfortunately this feature is not currently supported by Terraform, so we had to implement it here. 104 var glancePublicURL string 105 { 106 // Authenticate in OpenStack, get the token and extract the service catalog 107 var serviceCatalog *tokens.ServiceCatalog 108 { 109 authResult := conn.GetAuthResult() 110 auth, ok := authResult.(tokens.CreateResult) 111 if !ok { 112 return nil, fmt.Errorf("unable to extract service catalog") 113 } 114 115 var err error 116 serviceCatalog, err = auth.ExtractServiceCatalog() 117 if err != nil { 118 return nil, err 119 } 120 } 121 clientConfigCloud, err := clientconfig.GetCloudFromYAML(openstackdefaults.DefaultClientOpts(cloud)) 122 if err != nil { 123 return nil, err 124 } 125 glancePublicURL, err = gophercloud_openstack.V3EndpointURL(serviceCatalog, gophercloud.EndpointOpts{ 126 Type: "image", 127 Availability: gophercloud.AvailabilityPublic, 128 Region: clientConfigCloud.RegionName, 129 }) 130 if err != nil { 131 return nil, fmt.Errorf("cannot retrieve Glance URL from the service catalog: %w", err) 132 } 133 } 134 135 // upload the bootstrap Ignition config in Glance and save its location 136 var bootstrapConfigURL string 137 { 138 img, err := images.Create(ctx, conn, images.CreateOpts{ 139 Name: infraID + "-ignition", 140 ContainerFormat: "bare", 141 DiskFormat: "raw", 142 Tags: []string{"openshiftClusterID=" + infraID}, 143 }).Extract() 144 if err != nil { 145 return nil, fmt.Errorf("unable to create a Glance image for the bootstrap server's Ignition file: %w", err) 146 } 147 148 if res := imagedata.Upload(ctx, conn, img.ID, bytes.NewReader(bootstrapIgn)); res.Err != nil { 149 return nil, fmt.Errorf("unable to upload a Glance image for the bootstrap server's Ignition file: %w", res.Err) 150 } 151 152 bootstrapConfigURL = glancePublicURL + img.File 153 } 154 155 // To allow Ignition to download its config on the bootstrap machine from a location secured by a 156 // self-signed certificate, we have to provide it a valid custom ca bundle. 157 // To do so we generate a small ignition config that contains just Security section with the bundle 158 // and later append it to the main ignition config. 159 tokenID, err := conn.GetAuthResult().ExtractTokenID() 160 if err != nil { 161 return nil, fmt.Errorf("unable to extract an OpenStack token: %w", err) 162 } 163 164 caRefs, err := parseCertificateBundle(userCA) 165 if err != nil { 166 return nil, err 167 } 168 169 var ignProxy igntypes.Proxy 170 if proxy != nil { 171 if proxy.HTTPProxy != "" { 172 ignProxy.HTTPProxy = &proxy.HTTPProxy 173 } 174 if proxy.HTTPSProxy != "" { 175 ignProxy.HTTPSProxy = &proxy.HTTPSProxy 176 } 177 if proxy.NoProxy != "" { 178 noProxy := strings.Split(proxy.NoProxy, ",") 179 ignProxy.NoProxy = make([]igntypes.NoProxyItem, len(noProxy)) 180 for i := range noProxy { 181 ignProxy.NoProxy[i] = igntypes.NoProxyItem(noProxy[i]) 182 } 183 } 184 } 185 186 data, err := ignition.Marshal(igntypes.Config{ 187 Ignition: igntypes.Ignition{ 188 Version: igntypes.MaxVersion.String(), 189 Timeouts: igntypes.Timeouts{ 190 HTTPResponseHeaders: ptr.To(120), 191 }, 192 Security: igntypes.Security{ 193 TLS: igntypes.TLS{ 194 CertificateAuthorities: caRefs, 195 }, 196 }, 197 Config: igntypes.IgnitionConfig{ 198 Merge: []igntypes.Resource{ 199 { 200 Source: &bootstrapConfigURL, 201 HTTPHeaders: []igntypes.HTTPHeader{ 202 { 203 Name: "X-Auth-Token", 204 Value: &tokenID, 205 }, 206 }, 207 }, 208 }, 209 }, 210 Proxy: ignProxy, 211 }, 212 Storage: igntypes.Storage{ 213 Files: []igntypes.File{ 214 { 215 Node: igntypes.Node{ 216 Path: "/etc/hostname", 217 Overwrite: ptr.To(true), 218 }, 219 FileEmbedded1: igntypes.FileEmbedded1{ 220 Mode: ptr.To(420), 221 Contents: igntypes.Resource{ 222 Source: ptr.To(dataurl.EncodeBytes([]byte(infraID + "bootstrap"))), 223 }, 224 }, 225 }, 226 { 227 Node: igntypes.Node{ 228 Path: "/opt/openshift/tls/cloud-ca-cert.pem", 229 Overwrite: ptr.To(true), 230 }, 231 FileEmbedded1: igntypes.FileEmbedded1{ 232 Mode: ptr.To(420), 233 Contents: igntypes.Resource{ 234 Source: ptr.To(dataurl.EncodeBytes(userCA)), 235 }, 236 }, 237 }, 238 }, 239 }, 240 }) 241 if err != nil { 242 return nil, fmt.Errorf("unable to encode the Ignition shim: %w", err) 243 } 244 245 // Check the size of the base64-rendered ignition shim isn't to big for nova 246 // https://docs.openstack.org/nova/latest/user/metadata.html#user-data 247 if len(base64.StdEncoding.EncodeToString(data)) > 65535 { 248 return nil, fmt.Errorf("rendered bootstrap ignition shim exceeds the 64KB limit for nova user data -- try reducing the size of your CA cert bundle") 249 } 250 return data, nil 251 } 252 253 // ParseCertificateBundle loads each certificate in the bundle to the Ignition 254 // carrier type, ignoring any invisible character before, after and in between 255 // certificates. 256 func parseCertificateBundle(userCA []byte) ([]igntypes.Resource, error) { 257 var caRefs []igntypes.Resource 258 userCA = bytes.TrimSpace(userCA) 259 for len(userCA) > 0 { 260 var block *pem.Block 261 block, userCA = pem.Decode(userCA) 262 if block == nil { 263 return nil, fmt.Errorf("unable to parse certificate, please check the cacert section of clouds.yaml") 264 } 265 caRefs = append(caRefs, igntypes.Resource{Source: ptr.To(dataurl.EncodeBytes(pem.EncodeToMemory(block)))}) 266 userCA = bytes.TrimSpace(userCA) 267 } 268 return caRefs, nil 269 }