CreateInstanceTask.groovy
001 /*
002  * SPDX-License-Identifier: Apache-2.0
003  *
004  * Copyright 2019-2022 Andres Almiray.
005  *
006  * Licensed under the Apache License, Version 2.0 (the "License");
007  * you may not use this file except in compliance with the License.
008  * You may obtain a copy of the License at
009  *
010  *     http://www.apache.org/licenses/LICENSE-2.0
011  *
012  * Unless required by applicable law or agreed to in writing, software
013  * distributed under the License is distributed on an "AS IS" BASIS,
014  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015  * See the License for the specific language governing permissions and
016  * limitations under the License.
017  */
018 package org.kordamp.gradle.plugin.oci.tasks.create
019 
020 import com.oracle.bmc.core.BlockstorageClient
021 import com.oracle.bmc.core.ComputeClient
022 import com.oracle.bmc.core.VirtualNetworkClient
023 import com.oracle.bmc.core.model.BootVolume
024 import com.oracle.bmc.core.model.BootVolumeSourceDetails
025 import com.oracle.bmc.core.model.BootVolumeSourceFromBootVolumeDetails
026 import com.oracle.bmc.core.model.CreateBootVolumeDetails
027 import com.oracle.bmc.core.model.CreateVnicDetails
028 import com.oracle.bmc.core.model.Image
029 import com.oracle.bmc.core.model.Instance
030 import com.oracle.bmc.core.model.InstanceSourceViaImageDetails
031 import com.oracle.bmc.core.model.LaunchInstanceAgentConfigDetails
032 import com.oracle.bmc.core.model.LaunchInstanceDetails
033 import com.oracle.bmc.core.model.Shape
034 import com.oracle.bmc.core.model.Subnet
035 import com.oracle.bmc.core.requests.CreateBootVolumeRequest
036 import com.oracle.bmc.core.requests.GetBootVolumeRequest
037 import com.oracle.bmc.core.requests.GetInstanceRequest
038 import com.oracle.bmc.core.requests.GetSubnetRequest
039 import com.oracle.bmc.core.requests.LaunchInstanceRequest
040 import com.oracle.bmc.core.requests.ListBootVolumesRequest
041 import com.oracle.bmc.core.requests.ListInstancesRequest
042 import com.oracle.bmc.core.requests.ListShapesRequest
043 import com.oracle.bmc.core.requests.ListSubnetsRequest
044 import com.oracle.bmc.identity.IdentityClient
045 import com.oracle.bmc.identity.model.AvailabilityDomain
046 import com.oracle.bmc.identity.requests.ListAvailabilityDomainsRequest
047 import groovy.transform.CompileStatic
048 import org.apache.commons.codec.binary.Base64
049 import org.gradle.api.provider.Property
050 import org.gradle.api.tasks.Internal
051 import org.kordamp.gradle.plugin.oci.tasks.AbstractOCITask
052 import org.kordamp.gradle.plugin.oci.tasks.interfaces.OCITask
053 import org.kordamp.gradle.plugin.oci.tasks.traits.CompartmentIdAwareTrait
054 import org.kordamp.gradle.plugin.oci.tasks.traits.ImageAwareTrait
055 import org.kordamp.gradle.plugin.oci.tasks.traits.InstanceNameAwareTrait
056 import org.kordamp.gradle.plugin.oci.tasks.traits.OptionalUserDataFileAwareTrait
057 import org.kordamp.gradle.plugin.oci.tasks.traits.PublicKeyFileAwareTrait
058 import org.kordamp.gradle.plugin.oci.tasks.traits.ShapeAwareTrait
059 import org.kordamp.gradle.plugin.oci.tasks.traits.SubnetIdAwareTrait
060 import org.kordamp.gradle.plugin.oci.tasks.traits.VerboseAwareTrait
061 import org.kordamp.jipsy.annotations.TypeProviderFor
062 
063 import static org.kordamp.gradle.plugin.oci.tasks.create.CreateInstanceConsoleConnectionTask.maybeCreateInstanceConsoleConnection
064 import static org.kordamp.gradle.plugin.oci.tasks.get.GetInstancePublicIpTask.getInstancePublicIp
065 import static org.kordamp.gradle.plugin.oci.tasks.printers.BootVolumePrinter.printBootVolume
066 import static org.kordamp.gradle.plugin.oci.tasks.printers.InstancePrinter.printInstance
067 import static org.kordamp.gradle.util.StringUtils.isBlank
068 import static org.kordamp.gradle.util.StringUtils.isNotBlank
069 
070 /**
071  @author Andres Almiray
072  @since 0.1.0
073  */
074 @CompileStatic
075 @TypeProviderFor(OCITask)
076 class CreateInstanceTask extends AbstractOCITask implements CompartmentIdAwareTrait,
077     SubnetIdAwareTrait,
078     InstanceNameAwareTrait,
079     ImageAwareTrait,
080     ShapeAwareTrait,
081     PublicKeyFileAwareTrait,
082     OptionalUserDataFileAwareTrait,
083     VerboseAwareTrait {
084     static final String TASK_DESCRIPTION = 'Creates an Instance.'
085 
086     private final Property<String> createdInstanceId = project.objects.property(String)
087 
088     @Internal
089     Property<String> getCreatedInstanceId() {
090         this.@createdInstanceId
091     }
092 
093     @Override
094     void doExecuteTask() {
095         validateCompartmentId()
096         validateSubnetId()
097         validateImage()
098         validateShape()
099         validatePublicKeyFile()
100 
101         ComputeClient computeClient = createComputeClient()
102 
103         Image _image = validateImage(computeClient, getResolvedCompartmentId().get())
104         Shape _shape = validateShape(computeClient, getResolvedCompartmentId().get())
105 
106         File publicKeyFile = getResolvedPublicKeyFile().get().asFile
107         File userDataFile = getResolvedUserDataFile()?.get()?.asFile
108         String kmsKeyId = ''
109 
110         VirtualNetworkClient vcnClient = createVirtualNetworkClient()
111         BlockstorageClient blockstorageClient = createBlockstorageClient()
112         IdentityClient identityClient = createIdentityClient()
113 
114         Subnet subnet = vcnClient.getSubnet(GetSubnetRequest.builder()
115             .subnetId(getResolvedSubnetId().get())
116             .build())
117             .subnet
118 
119         Instance instance = maybeCreateInstance(this,
120             computeClient,
121             vcnClient,
122             blockstorageClient,
123             identityClient,
124             getResolvedCompartmentId().get(),
125             getResolvedInstanceName().get(),
126             _image,
127             _shape,
128             null,
129             subnet,
130             publicKeyFile,
131             userDataFile,
132             kmsKeyId,
133             getResolvedVerbose().get())
134         createdInstanceId.set(instance.id)
135     }
136 
137     static Instance maybeCreateInstance(OCITask owner,
138                                         ComputeClient computeClient,
139                                         VirtualNetworkClient vcnClient,
140                                         BlockstorageClient blockstorageClient,
141                                         IdentityClient identityClient,
142                                         String compartmentId,
143                                         String instanceName,
144                                         Image image,
145                                         Shape shape,
146                                         AvailabilityDomain availabilityDomain,
147                                         Subnet subnet,
148                                         File publicKeyFile,
149                                         File userDataFile,
150                                         String kmsKeyId,
151                                         boolean verbose) {
152         AvailabilityDomain ad = availabilityDomain ?: findMatchingAvailabilityDomain(
153             owner,
154             computeClient,
155             identityClient,
156             compartmentId,
157             shape.shape)
158 
159         subnet = subnet ?: findMatchingSubnet(owner,
160             vcnClient,
161             compartmentId,
162             subnet.vcnId,
163             ad)
164 
165         Instance instance = doMaybeCreateInstance(owner,
166             computeClient,
167             vcnClient,
168             compartmentId,
169             instanceName,
170             ad?.name ?: subnet.availabilityDomain,
171             image.id,
172             shape.shape,
173             subnet.id,
174             publicKeyFile,
175             userDataFile,
176             kmsKeyId,
177             verbose)
178 
179         maybeCreateInstanceConsoleConnection(owner,
180             computeClient,
181             compartmentId,
182             instance.id,
183             publicKeyFile,
184             true,
185             verbose)
186 
187         maybeCreateBootVolume(owner,
188             blockstorageClient,
189             compartmentId,
190             ad?.name ?: subnet.availabilityDomain,
191             instance.imageId,
192             instance.displayName + '-boot-volume',
193             kmsKeyId,
194             verbose)
195 
196         instance
197     }
198 
199     private static AvailabilityDomain findMatchingAvailabilityDomain(OCITask owner,
200                                                                      ComputeClient computeClient,
201                                                                      IdentityClient identityClient,
202                                                                      String compartmentId,
203                                                                      String shapeName) {
204         for (AvailabilityDomain availabilityDomain : identityClient.listAvailabilityDomains(ListAvailabilityDomainsRequest.builder()
205             .compartmentId(compartmentId)
206             .build()).items) {
207             for (Shape shape : computeClient.listShapes(ListShapesRequest.builder()
208                 .compartmentId(compartmentId)
209                 .availabilityDomain(availabilityDomain.name)
210                 .build()).items) {
211                 if (shape.shape == shapeName) {
212                     return availabilityDomain
213                 }
214             }
215         }
216 
217         null
218     }
219 
220     private static Subnet findMatchingSubnet(OCITask owner,
221                                              VirtualNetworkClient vcnClient,
222                                              String compartmentId,
223                                              String vcnId,
224                                              AvailabilityDomain availabilityDomain) {
225         for (Subnet subnet : vcnClient.listSubnets(ListSubnetsRequest.builder()
226             .compartmentId(compartmentId)
227             .vcnId(vcnId)
228             .build()).items) {
229             if (subnet.availabilityDomain == availabilityDomain.name) {
230                 return subnet
231             }
232         }
233 
234         null
235     }
236 
237     private static Instance doMaybeCreateInstance(OCITask owner,
238                                                   ComputeClient client,
239                                                   VirtualNetworkClient vcnClient,
240                                                   String compartmentId,
241                                                   String instanceName,
242                                                   String availabilityDomain,
243                                                   String imageId,
244                                                   String shape,
245                                                   String subnetId,
246                                                   File publicKeyFile,
247                                                   File userDataFile,
248                                                   String kmsKeyId,
249                                                   boolean verbose) {
250         // 1. Check if it exists
251         List<Instance> instances = client.listInstances(ListInstancesRequest.builder()
252             .compartmentId(compartmentId)
253             .availabilityDomain(availabilityDomain)
254             .displayName(instanceName)
255             .build())
256             .items
257 
258         if (!instances.empty) {
259             Instance instance = instances[0]
260             println("Instance '${instanceName}' already exists. id = ${owner.console.yellow(instance.id)}")
261             if (verboseprintInstance(owner, instance, 0)
262             return instances[0]
263         }
264 
265         Map<String, String> metadata = new HashMap<>('ssh_authorized_keys': publicKeyFile.text)
266         owner.printKeyValue("Public key file", publicKeyFile.absolutePath, 0)
267         if (userDataFile && userDataFile?.exists() && isNotBlank(userDataFile?.text)) {
268             owner.printKeyValue("Cloud init script", userDataFile.absolutePath, 0)
269             metadata.put("user_data", Base64.encodeBase64String(userDataFile.getBytes()))
270         }
271 
272         InstanceSourceViaImageDetails details =
273             (isBlank(kmsKeyId))
274                 ? InstanceSourceViaImageDetails.builder()
275                 .imageId(imageId)
276                 .build()
277                 : InstanceSourceViaImageDetails.builder()
278                 .imageId(imageId)
279                 .kmsKeyId(kmsKeyId)
280                 .build()
281 
282         Instance instance = client.launchInstance(LaunchInstanceRequest.builder()
283             .launchInstanceDetails(LaunchInstanceDetails.builder()
284                 .availabilityDomain(availabilityDomain)
285                 .compartmentId(compartmentId)
286                 .displayName(instanceName)
287                 .metadata(metadata)
288                 .shape(shape)
289                 .sourceDetails(details)
290                 .createVnicDetails(CreateVnicDetails.builder()
291                     .subnetId(subnetId)
292                     .build())
293                 .agentConfig(LaunchInstanceAgentConfigDetails.builder()
294                     .isMonitoringDisabled(false)
295                     .build())
296                 .build())
297             .build())
298             .instance
299 
300         println("Waiting for Instance to be ${owner.state('Available')}")
301         client.waiters.forInstance(GetInstanceRequest.builder()
302             .instanceId(instance.id)
303             .build(),
304             Instance.LifecycleState.Running)
305             .execute()
306 
307         println("Instance '${instanceName}' has been provisioned. id = ${owner.console.yellow(instance.id)}")
308         if (verboseprintInstance(owner, instance, 0)
309 
310         Set<String> publicIps = getInstancePublicIp(owner,
311             client,
312             vcnClient,
313             compartmentId,
314             instance.id)
315 
316         owner.printCollection('Ip Addresses', publicIps, 0)
317 
318         instance
319     }
320 
321     private static BootVolume maybeCreateBootVolume(OCITask owner,
322                                                     BlockstorageClient client,
323                                                     String compartmentId,
324                                                     String availabilityDomain,
325                                                     String imageId,
326                                                     String displayName,
327                                                     String kmsKeyId,
328                                                     boolean verbose) {
329         List<BootVolume> bootVolumes = client.listBootVolumes(ListBootVolumesRequest.builder()
330             .availabilityDomain(availabilityDomain)
331             .compartmentId(compartmentId)
332             .build())
333             .items
334 
335         String bootVolumeId = null
336         for (BootVolume bootVolume : bootVolumes) {
337             if (bootVolume.lifecycleState.equals(BootVolume.LifecycleState.Available)
338                 && bootVolume.imageId != null
339                 && bootVolume.imageId.equals(imageId)
340                 && bootVolume.displayName.equals(displayName)) {
341                 return bootVolume
342             }
343             if (bootVolume.lifecycleState.equals(BootVolume.LifecycleState.Available)
344                 && bootVolume.imageId != null
345                 && bootVolume.imageId.equals(imageId)) {
346                 bootVolumeId = bootVolume.id
347                 break
348             }
349         }
350 
351         BootVolumeSourceDetails bootVolumeSourceDetails = BootVolumeSourceFromBootVolumeDetails.builder()
352             .id(bootVolumeId)
353             .build()
354 
355         CreateBootVolumeDetails.Builder details = CreateBootVolumeDetails.builder()
356             .availabilityDomain(availabilityDomain)
357             .compartmentId(compartmentId)
358             .displayName(displayName)
359             .sourceDetails(bootVolumeSourceDetails)
360 
361         if (isNotBlank(kmsKeyId)) {
362             details = details.kmsKeyId(kmsKeyId)
363         }
364 
365         BootVolume bootVolume = client.createBootVolume(CreateBootVolumeRequest.builder()
366             .createBootVolumeDetails(details.build())
367             .build())
368             .bootVolume
369 
370         println("Waiting for BootVolume to be ${owner.state('Available')}")
371         client.waiters.forBootVolume(
372             GetBootVolumeRequest.builder().bootVolumeId(bootVolumeId).build(),
373             BootVolume.LifecycleState.Available)
374             .execute()
375             .bootVolume
376 
377         println("BootVolume '${bootVolume.displayName}' has been provisioned. id = ${owner.console.yellow(bootVolume.id)}")
378         if (verboseprintBootVolume(owner, bootVolume, 0)
379         bootVolume
380     }
381 }