Skip to content

Instantly share code, notes, and snippets.

@dennischen
Last active December 11, 2019 03:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dennischen/a671866010185898a110861be1098891 to your computer and use it in GitHub Desktop.
Save dennischen/a671866010185898a110861be1098891 to your computer and use it in GitHub Desktop.
Cloud Hint
public class Functions {
public static final SecurityFunction CloudCenter = new SecurityFunction("CloudCenter",
(Set)Collections.asSet(new SimpleAction(Function.ACTION_EDIT)),
new SimpleUIInfo("~@/cloud/RegionCloudCenter.zul","~./cloud/imgs/function/FN_CloudCenter.png","l:cloud.CloudCenter"));
public static final SecurityFunction CloudRentalCenter = new SecurityFunction("CloudRentalCenter",
(Set)Collections.asSet(new SimpleAction(Function.ACTION_EDIT)),
new SimpleUIInfo("~@/cloud/RegionCloudRentalCenter.zul","~./cloud/imgs/function/FN_CloudRentalCenter.png","l:cloud.RentalCenter"));
public static final SecurityFunction CloudAdminCenter = new SecurityFunction("CloudAdminCenter",
(Set)Collections.asSet(new SimpleAction(Function.ACTION_ADMIN)),
new SimpleUIInfo("~@/cloud/CloudAdminCenter.zul","~./cloud/imgs/function/FN_CloudAdminCenter.png","l:cloud.CloudAdminCenter"));
public static final SecurityFunction BpmRequestCenter = new SecurityFunction("BpmRequestCenter",
(Set)Collections.asSet(new SimpleAction(Function.ACTION_EDIT),new SimpleAction(ACTION_APPROVER),new SimpleAction(ACTION_HELPER)),
new SimpleUIInfo("~@/cloud/BpmRequestCenter.zul","~./cloud/imgs/function/FN_BpmRequestCenter.png","l:bpm.RequestCenter"));
public static final SecurityFunction ComputerCenter = new SecurityFunction("ComputerCenter",
(Set)Collections.asSet(new SimpleAction(Function.ACTION_EDIT)),
new SimpleUIInfo("~@/cloud/RegionComputerCenter.zul","~./cloud/imgs/function/FN_ComputerCenter.png","l:cloud.ComputerCenter"));
public static final SecurityFunction CloudDriveCenter = new SecurityFunction("CloudDriveCenter",
(Set)Collections.asSet(new SimpleAction(Function.ACTION_EDIT)),
new SimpleUIInfo("~@/cloud/RegionCloudDriveCenter.zul","~./cloud/imgs/function/FN_CloudDriveCenter.png","l:cloud.CloudDriveCenter"));
static{
FunctionTable.registerFunction(CloudCenter);
FunctionTable.registerFunction(ComputerCenter);
FunctionTable.registerFunction(CloudDriveCenter);
FunctionTable.registerFunction(CloudRentalCenter);
FunctionTable.registerFunction(BpmRequestCenter);
FunctionTable.registerFunction(CloudAdminCenter);
}
}
<borderlayout id="main" hflex="1" vflex="1" sclass="borderless cloud" apply="com.zechster.cloud.ui.ctrl.RegionCloudCenterCtrl">
<west width="140px" minsize="140" splittable="true" collapsible="true" title="${z:l('cloud.region')}" sclass="regionTitle">
<listbox id="regionListbox" hflex="1" vflex="1" sclass="borderless functionList" forward="onSelect=main.onRegionListboxSelect"
emptyMessage="${z:l('zechspace.msg.emptyList')}">
<template name="model">
<listitem>
<listcell sclass="iconcell">
<i sclass="fa fa-lg fa-fw fa-globe" />
${z:pfxl(each.title)}
</listcell>
</listitem>
</template>
</listbox>
</west>
<center>
<include id="iaasCenterInclude"/>
</center>
</borderlayout>
<vlayout id="main" hflex="1" vflex="1" sclass="cloud">
<singletabbox hflex="1" vflex="1" sclass="paddingless-tabbox icontabbox bottomline"
keepSelection="true" id="deviceMgntTabbox">
<tabs>
<tab id="overviewTab" tooltiptext="${z:l('cloud.contractOverview')}" image="~./cloud/imgs/tab/contract.png" />
<tab id="computerTab" tooltiptext="${z:l('iaas.computer')}" image="~./cloud/imgs/tab/computer.png"
if="${cld:computerSupported(editingRegion.name)}"/>
<tab id="diskTab" tooltiptext="${z:l('iaas.disk')}" image="~./cloud/imgs/tab/disk.png"
if="${cld:diskSupported(editingRegion.name)}"/>
<tab id="backupQuotaTab" tooltiptext="${z:l('iaas.backupQuota')}" image="~./cloud/imgs/tab/backupquota.png"
if="${cld:backupQuotaSupported(editingRegion.name)}"/>
<tab id="extipTab" tooltiptext="${z:l('iaas.externalIp')}" image="~./cloud/imgs/tab/externalip.png"
if="${cld:externalIpSupported(editingRegion.name)}"/>
<tab id="snapshotTab" tooltiptext="${z:l('iaas.snapshot')}" image="~./cloud/imgs/tab/snapshot.png"
if="${cld:computerSnapshotSupported(editingRegion.name) or cld:diskSnapshotSupported(editingRegion.name)}"/>
<tab id="securityTab" tooltiptext="${z:l('iaas.security')}" image="~./cloud/imgs/tab/security.png"
if="${cld:keyPairSupported(editingRegion.name) or cld:securityGroupSupported(editingRegion.name)}"/>
<tab id="cloudDriveTab" tooltiptext="${z:l('iaas.cloudDrive')}" image="~./cloud/imgs/tab/clouddrive.png"
if="${cld:cloudDriveSupported(editingRegion.name)}"/>
<tab id="loadBalancerTab" tooltiptext="${z:l('iaas.loadBalancer')}" image="~./cloud/imgs/tab/loadbalancer.png"
if="${cld:loadBalancerSupported(editingRegion.name)}"/>
<tab id="usageTab" tooltiptext="${z:l('cloud.usageSummary')}" image="~./cloud/imgs/tab/usage.png" />
</tabs>
<tabpanels>
<singletabpanel sclass="borderless" src="~@/cloud/contract/RentalContractOverviewPanel.zul" >
</singletabpanel>
<singletabpanel sclass="borderless" src="~@/cloud/iaas/virt/device/ComputerMgntPanel.zul"
if="${cld:computerSupported(editingRegion.name)}">
</singletabpanel>
<singletabpanel sclass="borderless" src="~@/cloud/iaas/virt/device/DiskMgntPanel.zul"
if="${cld:diskSupported(editingRegion.name)}">
</singletabpanel>
<singletabpanel sclass="borderless" src="~@/cloud/iaas/virt/device/BackupQuotaMgntPanel.zul"
if="${cld:backupQuotaSupported(editingRegion.name)}">
</singletabpanel>
<singletabpanel sclass="borderless" src="~@/cloud/iaas/virt/device/ExternalIpMgntPanel.zul"
if="${cld:externalIpSupported(editingRegion.name)}">
</singletabpanel>
<singletabpanel sclass="borderless" src="~@/cloud/iaas/virt/device/SnapshotMgntPanel.zul"
if="${cld:computerSnapshotSupported(editingRegion.name) or cld:diskSnapshotSupported(editingRegion.name)}">
</singletabpanel>
<singletabpanel sclass="borderless" src="~@/cloud/iaas/virt/device/SecurityMgntPanel.zul"
if="${cld:keyPairSupported(editingRegion.name) or cld:securityGroupSupported(editingRegion.name)}">
</singletabpanel>
<singletabpanel sclass="borderless" src="~@/cloud/saas/cdr/device/CloudDriveMgntPanel.zul"
if="${cld:cloudDriveSupported(editingRegion.name)}">
</singletabpanel>
<singletabpanel sclass="borderless" src="~@/cloud/iaas/virt/device/LoadBalancerMgntPanel.zul"
if="${cld:loadBalancerSupported(editingRegion.name)}">
</singletabpanel>
<singletabpanel sclass="borderless" src="~@/cloud/device/DeviceUsageSummaryPanel.zul">
</singletabpanel>
</tabpanels>
</singletabbox>
</vlayout>
<vlayout id="main" hflex="1" vflex="1" spacing="0px" apply="com.zechster.cloud.ui.ctrl.iaas.virt.device.ComputerMgntPanelCtrl">
<toolbar hflex="1" >
<hbox hflex="1">
<hlayout hflex="min" if="${cloudAdminCenter}">
<toolbarbutton sclass="x24btnOnly" image="~./zechspace/imgs/x24/more.png" xmlns:w="client"
w:onClick="zechspace.toggleVisibleIfAny(this.$f('conditionBox'));zechspace.toggleVisibleIfAny(this.$f('moreConditionBox'));zechspace.toggleVisibleIfAny(this.$f('userBox'));zechspace.fireOnSizeIfAny(this.$f('main'))" />
</hlayout>
<hbox align="center" hflex="1" id="userBox" if="${cloudAdminCenter}">
<toolbarbutton id="selectUserBtn" tooltiptext="${c:l('zechguard.selectUser')}" sclass="x24btnOnly"
image="~./zechguard/imgs/x24/user.png" forward="onClick=main.onSelectUser">
</toolbarbutton>
<label id="userNameLab"/>
</hbox>
<hbox align="center" id="conditionBox" hflex="3" pack="end">
<listbox id="statusListbox" mold="select" sclass="formInput" forward="onSelect=main.onNormalSearch">
<template name="model">
<listitem label="${each.label}" />
</template>
</listbox>
<textbox id="keywordBox" sclass="formInput" forward="onOK=main.onNormalSearch,onChange=main.onNormalSearch"/>
<toolbarbutton id="searchBtn" tooltiptext="${z:l('zechspace.search')}" forward="onClick=main.onNormalSearch" sclass="x24btnOnly"
image="~./zechspace/imgs/x24/search.png" />
<toolbarbutton id="newBtn" tooltiptext="${c:l('zechspace.new')}" if="${cloudAdminCenter}"
forward="onClick=main.onNewDevice" sclass="x24btnOnly" image="~./zechspace/imgs/x24/add.png"/>
</hbox>
</hbox>
</toolbar>
<hlayout id="moreConditionBox" sclass="cloudConditionBox" visible="false" if="${cloudAdminCenter}">
<grid hflex="1" forward="onOK=main.onIndexSearch">
<columns>
<column hflex="min" />
<column hflex="1"/>
<column hflex="min" />
<column hflex="2"/>
</columns>
<rows>
<row>
<label sclass="formTitle" value="${z:l('iaas.computer.name')}" />
<textbox id="indexKeywordBox" sclass="formInput" forward="onChange=main.onIndexSearch"/>
<cell colspan="2">
<hbox pack="end" hflex="1">
<toolbarbutton tooltiptext="${z:l('zechspace.search')}" forward="onClick=main.onIndexSearch" sclass="x24btnOnly"
image="~./zechspace/imgs/x24/search.png" />
</hbox>
</cell>
</row>
<row>
<label sclass="formTitle" value="${z:l('iaas.computerSpec')}" />
<listbox id="indexComputerSpecListbox" mold="select" sclass="formInput" hflex="1" forward="onSelect=main.onIndexSearch">
<template name="model">
<listitem label="${each.label}"/>
</template>
</listbox>
<label sclass="formTitle" value="${z:l('iaas.bootImage')}" />
<listbox id="indexBootImageListbox" mold="select" sclass="formInput" hflex="1" forward="onSelect=main.onIndexSearch">
<template name="model">
<listitem label="${each.label}"/>
</template>
</listbox>
</row>
<row>
<label sclass="formTitle" value="${z:l('iaas.computer.application')}" />
<textbox id="indexApplicationBox" sclass="formInput" forward="onChange=main.onIndexSearch"/>
</row>
</rows>
</grid>
</hlayout>
<grid sclass="cloudGrid wideRow" hflex="1" vflex="1" id="computerGrid" emptyMessage="${z:l('zechspace.msg.emptyList')}">
<columns sizable="true">
<column hflex="min" align="center" />
<column hflex="1" label="${z:l('iaas.computer.name')}" forward="onClick=main.onColumnSort(name)" sclass="sortable" />
<column hflex="2" label="${z:l('iaas.ip')}" />
<column hflex="2" label="${z:l('iaas.externalIp')}" />
<column hflex="1" label="${z:l('iaas.computer.status')}" align="center" />
<column hflex="1" label="${z:l('iaas.computer.uptime')}" forward="onClick=main.onColumnSort(createdTime)" sclass="sortable"/>
<column hflex="1" label="${z:l('zechspace.owner')}" if="${cloudAdminCenter}" />
<column hflex="min" />
</columns>
<template name="model">
<row>
<vlayout >
<i sclass="fa fa-2x ${mainCtrl.getStatusSclass(each.computer)}" />
<i sclass="fa fa-2x ${mainCtrl.getCpuGpuSclass(each.computer)}" />
</vlayout>
<label value="${each.computer.name}" sclass="label-clickable" forward="onClick=main.onDetail(${each})"/>
<html content="${each.insideAddresses}" sclass="markdown"/>
<html content="${each.outsideAddresses}" sclass="markdown"/>
<vbox align="center" width="100%" spacing="0px">
<image src="~./zechspace/imgs/loading.gif" if="${mainCtrl.isProcessing(each.computer)}"/>
<label value="${mainCtrl.getStatusOrProcessing(each.computer)}" />
</vbox>
<label value="${mainCtrl.getUptimeInfo(each.computer)}" />
<label value="${mainCtrl.getOwnerInfo(each.computer)}" if="${cloudAdminCenter}"/>
<cell>
<toolbarbutton self="@seq-id(computerFnBtn)"
disabled="${mainCtrl.isProcessing(each.computer)}" sclass="x24btnOnly" image="~./zechspace/imgs/x24/more.png"
forward="onClick=main.onFnMenu(${each})"/>
</cell>
</row>
</template>
</grid>
<pagingbox id="pagingBox" align="center" pack="start" hflex="1" sclass="button-box-thin"
visible="false"
forward="onFirstPage=main.onFirstPage,onPrevPage=main.onPrevPage,onNextPage=main.onNextPage" />
<div>
<menupopup id="fnMenu">
<menuitem label="${z:l('iaas.computer.fn.detail')}" id="fnDetail"
forward="onClick=main.onFnDetail" />
<menuitem label="${z:l('iaas.computer.fn.rename')}" id="fnRename"
forward="onClick=main.onFnRename" />
<menuseparator />
<menuitem label="${z:l('iaas.computer.fn.openConsole')}" id="fnOpenConsole"
forward="onClick=main.onFnOpenConsole" iconSclass="fa fa-desktop" />
<menuitem label="${z:l('iaas.computer.fn.queryPassword')}" id="fnQueryPassword"
forward="onClick=main.onFnQueryPassword" iconSclass="fa fa-key" />
<menuitem label="${z:l('iaas.computer.fn.start')}" id="fnStart"
forward="onClick=main.onFnStart" iconSclass="fa fa-play"/>
<menuitem label="${z:l('iaas.computer.fn.stop')}" id="fnStop"
forward="onClick=main.onFnStop" iconSclass="fa fa-stop"/>
<menuitem label="${z:l('iaas.computer.fn.suspend')}" id="fnSuspend"
forward="onClick=main.onFnSuspend" />
<menuitem label="${z:l('iaas.computer.fn.resume')}" id="fnResume"
forward="onClick=main.onFnResume" />
<menuitem label="${z:l('iaas.computer.fn.pause')}" id="fnPause"
forward="onClick=main.onFnPause" />
<menuitem label="${z:l('iaas.computer.fn.unpause')}" id="fnUnpause"
forward="onClick=main.onFnUnpause" />
<menuitem label="${z:l('iaas.computer.fn.reboot')}" id="fnReboot"
forward="onClick=main.onFnReboot" />
<menuitem label="${z:l('iaas.computer.fn.hardReboot')}" id="fnHardReboot"
forward="onClick=main.onFnHardReboot" />
<menuseparator />
<menuitem label="${z:l('iaas.computer.fn.takeSnapshot')}" id="fnTakeSnapshot"
forward="onClick=main.onFnTakeSnapshot"
if="${cld:computerSnapshotSupported(editingRegion.name)}"/>
<menu label="${z:l('iaas.computer.fn.restoreSnapshot')}" id="fnRestoreSnapshot"
if="${cld:computerSnapshotSupported(editingRegion.name)}"/>
<menuseparator />
<menu label="${z:l('iaas.computer.fn.attachDisk')}" id="fnAssignDisk"
if="${cld:diskSupported(editingRegion.name)}"/>
<menu label="${z:l('iaas.computer.fn.setExternalIp')}" id="fnSetExternalIp"
if="${cld:externalIpSupported(editingRegion.name)}"/>
<menu label="${z:l('iaas.computer.fn.setSecurityGroup')}" id="fnSetSecurityGroup"
if="${cld:securityGroupSupported(editingRegion.name)}"/>
<menuseparator />
<menuitem label="${z:l('iaas.computer.fn.addMonitor')}" id="fnAddMonitor"
forward="onClick=main.onFnAddMonitor"/>
<menuseparator if="${cloudAdminCenter}" />
<menuitem label="${z:l('iaas.computer.fn.migrate')}" if="${cloudAdminCenter}"
id="fnMigrate" forward="onClick=main.onFnMigrate" />
<menuitem label="${z:l('iaas.computer.fn.liveMigrate')}" if="${cloudAdminCenter}"
id="fnLiveMigrate" forward="onClick=main.onFnLiveMigrate" />
<menuitem label="${z:l('cloud.device.fn.upgrade')}" if="${cloudAdminCenter}"
id="fnUpgrade" forward="onClick=main.onFnUpgrade" />
<menuseparator />
<menuitem label="${z:l('cloud.device.fn.maintainOn')}" if="${cloudAdminCenter}"
id="fnMaintainOn" forward="onClick=main.onFnMaintainOn" />
<menuitem label="${z:l('cloud.device.fn.maintainOff')}" if="${cloudAdminCenter}"
id="fnMaintainOff" forward="onClick=main.onFnMaintainOff" />
<menuitem label="${z:l('cloud.device.fn.scheduleDelete')}" if="${cloudAdminCenter}"
id="fnDelete" forward="onClick=main.onFnDelete" />
</menupopup>
</div>
</vlayout>
public interface VirtUserService {
UserQuotaController getUserQuotaController(String regionName,String userUuid);
DiskController getDiskController(String regionName,String userUuid);
DiskSnapshotController getDiskSnapshotController(String regionName,String userUuid);
ComputerController getComputerController(String regionName,String userUuid);
ComputerSnapshotController getComputerSnapshotController(String region, String userUuid);
ExternalIpController getExternalIpController(String regionName,String userUuid);
KeyPairController getKeyPairController(String regionName,String userUuid);
SecurityGroupController getSecurityGroupController(String regionName,String userUuid);
BackupQuotaController getBackupQuotaController(String regionName,String userUuid);
LoadBalancerController getLoadBalancerController(String regionName,String userUuid);
boolean isComputerUpgradeSupported(String regionName);
boolean isDiskSupported(String regionName);
boolean isDiskUpgradeSupported(String regionName);
boolean isExternalIpSupported(String regionName);
boolean isComputerSnapshotSupported(String regionName);
boolean isDiskSnapshotSupported(String regionName);
boolean isKeyPairSupported(String regionName);
boolean isSecurityGroupSupported(String regionName);
boolean isBackupQuotaSupported(String regionName);
boolean isBackupQuotaUpgradeSupported(String regionName);
boolean isLoadBalancerSupported(String regionName);
}
public interface ComputerController extends LiteComputerController {
ComputerCreator creator();
Computer restoreFromSnapshot(String snapshotId, String name) throws ActionFailException;
ComputerUpdater updater(String id);
ComputerUpgrader upgrader(String id);
Computer getByGroupId(String groupId);
void delete(String id) throws ActionFailException;
Computer waitForNullStatus(String id, Set<Computer.Status> statusSet, Set<Computer.Status> breakStatusSet, long timeout);
Set<String> getSuggestedTags(Computer computer);
public interface ComputerCreator {
Computer create() throws ActionFailException;
ComputerCreator name(String name);
ComputerCreator specId(String specId);
ComputerCreator bootImageId(String imageId);
ComputerCreator keypairName(String name);
ComputerCreator securityGroupName(String name);
}
public interface ComputerUpdater {
Computer update() throws ActionFailException;
ComputerUpdater name(String name);
ComputerUpdater securityGroupName(String name);
}
public interface ComputerUpgrader {
Computer upgrade() throws ActionFailException;
ComputerUpgrader specId(String specId);
}
boolean isRenameable(Computer computer);
boolean isTakeSnapshotable(Computer computer);
boolean isRestoreSnapshotable(Computer computer);
boolean isDeleteSnapshotable(Computer computer);
boolean isAssignDiskable(Computer computer);
boolean isSetExternalIpable(Computer computer);
boolean isSetSecurityGroupable(Computer computer);
boolean isMigrateable(Computer computer);
boolean isLiveMigrateable(Computer computer);
boolean isUpgradable(Computer computer);
void setMaintenance(String computerId, MaintenanceCode code) throws ActionFailException;
}
package com.zechster.iaas.services.virt;
import com.zechster.cloud.services.Sweeper;
import com.zechster.cloud.services.region.Region;
import com.zechster.iaas.services.virt.device.BackupQuotaController;
import com.zechster.iaas.services.virt.device.ComputerController;
import com.zechster.iaas.services.virt.device.ComputerSnapshotController;
import com.zechster.iaas.services.virt.device.DiskController;
import com.zechster.iaas.services.virt.device.DiskSnapshotController;
import com.zechster.iaas.services.virt.device.ExternalIpController;
import com.zechster.iaas.services.virt.device.KeyPairController;
import com.zechster.iaas.services.virt.device.LiteComputerController;
import com.zechster.iaas.services.virt.device.LoadBalancerController;
import com.zechster.iaas.services.virt.device.UserQuotaController;
import com.zechster.iaas.services.virt.device.SecurityGroupController;
/**
*
* @author Dennis Chen
*
*/
public interface VirtControllerProvider {
//device controllers
DiskController getDiskController(Region region, String userUuid);
DiskSnapshotController getDiskSnapshotController(Region region, String userUuid);
ComputerController getComputerController(Region region, String userUuid);
ComputerSnapshotController getComputerSnapshotController(Region region, String userUuid);
BackupQuotaController getBackupQuotaController(Region region, String userUuid);
ExternalIpController getExternalIpController(Region region, String userUuid);
KeyPairController getKeyPairController(Region region, String userUuid);
SecurityGroupController getSecurityGroupController(Region region, String userUuid);
UserQuotaController getUserQuotaController(Region region, String userUuid);
LoadBalancerController getLoadBalancerController(Region region, String userUuid);
//lite controllers
LiteComputerController getLiteComputerController(Region region, String userUuid);
//limition
boolean isComputerUpgradeSupported(Region region);
boolean isDiskSupported(Region region);
boolean isDiskUpgradeSupported(Region region);
boolean isBackupQuotaSupported(Region region);
boolean isBackupQuotaUpgradeSupported(Region region);
boolean isExternalIpSupported(Region region);
boolean isComputerSnapshotSupported(Region region);
boolean isDiskSnapshotSupported(Region region);
boolean isKeyPairSupported(Region region);
boolean isSecurityGroupSupported(Region region);
boolean isLoadBalancerSupported(Region region);
//infra controllers
ComputerSpecController getComputerSpecController(Region region);
BootImageController getBootImageController(Region region);
UserSpaceController getUserSpaceController(Region region);
HypervisorController getHypervisorController(Region region);
SystemInformation getSystemInformation(Region region);
Sweeper getSweeper(Region region);
AutoMigrationController getAutoMigrationController(Region region);
DefaultUserQuotaController getDefaultUserQuotaController(Region region);
}
public class OpenStackVirtControllerProvider implements VirtControllerProvider {
}
public class ComputeComputerController extends ComputeLiteComputerController implements ComputerController {
}
<?xml version="1.0" encoding="UTF-8" ?>
<config >
<cloud>
<regions>
<region name="openstack">
<title>Openstack</title>
<capability value="openstack">Virtualization</capability>
<capability>ComputerCenter</capability>
<capability>DeviceUpgrade</capability>
<openstack>
<consoleType>web-url</consoleType>
<externalNetworkName>external</externalNetworkName>
<managementNetworkName>management</managementNetworkName>
<cpuZone>nova</cpuZone>
<gpuZone>nova</gpuZone>
</openstack>
</region>
</regions>
</cloud>
<openstacks>
<openstack name="openstack">
<endpoint>http://192.168.220.61:5000/v3</endpoint>
<username>admin</username>
<password>***************</password>
<defaultTenantName>admin</defaultTenantName>
</openstack>
</openstacks>
</config>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment