Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(upgrade): support V2 volume upgrade #2745

Merged
merged 3 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 23 additions & 15 deletions controller/engine_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,9 @@ func (ec *EngineController) CreateInstance(obj interface{}) (*longhorn.InstanceP
DataLocality: v.Spec.DataLocality,
ImIP: im.Status.IP,
EngineCLIAPIVersion: cliAPIVersion,
UpgradeRequired: false,
InitiatorAddress: im.Status.IP,
TargetAddress: im.Status.IP,
})
}

Expand Down Expand Up @@ -2119,25 +2122,30 @@ func (ec *EngineController) Upgrade(e *longhorn.Engine, log *logrus.Entry) (err
err = errors.Wrapf(err, "failed to live upgrade image for %v", e.Name)
}()

engineClientProxy, err := ec.getEngineClientProxy(e, e.Spec.Image)
if err != nil {
return err
}
defer engineClientProxy.Close()

version, err := engineClientProxy.VersionGet(e, false)
if err != nil {
return err
}
if types.IsDataEngineV1(e.Spec.DataEngine) {
engineClientProxy, err := ec.getEngineClientProxy(e, e.Spec.Image)
if err != nil {
return err
}
defer engineClientProxy.Close()

// Don't use image with different image name but same commit here. It
// will cause live replica to be removed. Volume controller should filter those.
if version.ClientVersion.GitCommit != version.ServerVersion.GitCommit {
log.Infof("Upgrading engine from %v to %v", e.Status.CurrentImage, e.Spec.Image)
if err := ec.UpgradeEngineInstance(e, log); err != nil {
version, err := engineClientProxy.VersionGet(e, false)
if err != nil {
return err
}

// Don't use image with different image name but same commit here. It
// will cause live replica to be removed. Volume controller should filter those.
if version.ClientVersion.GitCommit != version.ServerVersion.GitCommit {
log.Infof("Upgrading engine from %v to %v", e.Status.CurrentImage, e.Spec.Image)
if err := ec.UpgradeEngineInstance(e, log); err != nil {
return err
}
}
} else {
return errors.Wrapf(err, "upgrading engine %v with data engine %v is not supported", e.Name, e.Spec.DataEngine)
}

log.Infof("Engine has been upgraded from %v to %v", e.Status.CurrentImage, e.Spec.Image)
e.Status.CurrentImage = e.Spec.Image
e.Status.CurrentReplicaAddressMap = e.Spec.UpgradedReplicaAddressMap
Expand Down
11 changes: 7 additions & 4 deletions controller/instance_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ func (h *InstanceHandler) ReconcileInstanceState(obj interface{}, spec *longhorn
return err
}

log := logrus.WithField("instance", instanceName)

isCLIAPIVersionOne := false
if types.IsDataEngineV1(spec.DataEngine) {
if status.CurrentImage != "" {
Expand Down Expand Up @@ -296,11 +298,11 @@ func (h *InstanceHandler) ReconcileInstanceState(obj interface{}, spec *longhorn
if !status.LogFetched {
// No need to get the log for instance manager if the data engine is not "longhorn"
if types.IsDataEngineV1(spec.DataEngine) {
logrus.Warnf("Getting requested log for %v in instance manager %v", instanceName, status.InstanceManagerName)
log.Warnf("Getting requested log for %v in instance manager %v", instanceName, status.InstanceManagerName)
if im == nil {
logrus.Warnf("Failed to get the log for %v due to Instance Manager is already gone", status.InstanceManagerName)
log.Warnf("Failed to get the log for %v due to Instance Manager is already gone", status.InstanceManagerName)
} else if err := h.printInstanceLogs(instanceName, runtimeObj); err != nil {
logrus.WithError(err).Warnf("Failed to get requested log for instance %v on node %v", instanceName, im.Spec.NodeID)
log.WithError(err).Warnf("Failed to get requested log for instance %v on node %v", instanceName, im.Spec.NodeID)
}
}
status.LogFetched = true
Expand Down Expand Up @@ -384,10 +386,11 @@ func (h *InstanceHandler) ReconcileInstanceState(obj interface{}, spec *longhorn
}
status.Started = false
default:
return fmt.Errorf("BUG: unknown instance desire state: desire %v", spec.DesireState)
return fmt.Errorf("unknown instance desire state: desire %v", spec.DesireState)
}

h.syncStatusWithInstanceManager(im, instanceName, spec, status, instances)

switch status.CurrentState {
case longhorn.InstanceStateRunning:
// If `spec.DesireState` is `longhorn.InstanceStateStopped`, `spec.NodeID` has been unset by volume controller.
Expand Down
2 changes: 1 addition & 1 deletion controller/replica_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,100 +207,100 @@
)
}

func (rc *ReplicaController) syncReplica(key string) (err error) {
defer func() {
err = errors.Wrapf(err, "failed to sync replica for %v", key)
}()
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
return err
}
if namespace != rc.namespace {
// Not ours, don't do anything
return nil
}

replica, err := rc.ds.GetReplica(name)
if err != nil {
if datastore.ErrorIsNotFound(err) {
return nil
}
return errors.Wrap(err, "failed to get replica")
}
dataPath := types.GetReplicaDataPath(replica.Spec.DiskPath, replica.Spec.DataDirectoryName)

log := getLoggerForReplica(rc.logger, replica)

if !rc.isResponsibleFor(replica) {
return nil
}
if replica.Status.OwnerID != rc.controllerID {
replica.Status.OwnerID = rc.controllerID
replica, err = rc.ds.UpdateReplicaStatus(replica)
if err != nil {
// we don't mind others coming first
if apierrors.IsConflict(errors.Cause(err)) {
return nil
}
return err
}
log.Infof("Replica got new owner %v", rc.controllerID)
}

if replica.DeletionTimestamp != nil {
if err := rc.DeleteInstance(replica); err != nil {
return errors.Wrapf(err, "failed to cleanup the related replica instance before deleting replica %v", replica.Name)
}

rs, err := rc.ds.ListVolumeReplicasRO(replica.Spec.VolumeName)
if err != nil {
return errors.Wrapf(err, "failed to list replicas of the volume before deleting replica %v", replica.Name)
}

if replica.Spec.NodeID != "" && replica.Spec.NodeID != rc.controllerID {
log.Warn("Failed to cleanup replica's data because the replica's data is not on this node")
} else if replica.Spec.NodeID != "" {
if types.IsDataEngineV1(replica.Spec.DataEngine) {
// Clean up the data directory if this is the active replica or if this inactive replica is the only one
// using it.
if (replica.Spec.Active || !hasMatchingReplica(replica, rs)) && dataPath != "" {
// prevent accidentally deletion
if !strings.Contains(filepath.Base(filepath.Clean(dataPath)), "-") {
return fmt.Errorf("%v doesn't look like a replica data path", dataPath)
}
log.Info("Cleaning up replica")
if err := lhns.DeleteDirectory(dataPath); err != nil {
return errors.Wrapf(err, "cannot cleanup after replica %v at %v", replica.Name, dataPath)
}
} else {
log.Info("Didn't cleanup replica since it's not the active one for the path or the path is empty")
}
}
}

return rc.ds.RemoveFinalizerForReplica(replica)
}

existingReplica := replica.DeepCopy()
defer func() {
// we're going to update replica assume things changes
if err == nil && !reflect.DeepEqual(existingReplica.Status, replica.Status) {
_, err = rc.ds.UpdateReplicaStatus(replica)
}
// requeue if it's conflict
if apierrors.IsConflict(errors.Cause(err)) {
log.WithError(err).Debugf("Requeue %v due to conflict", key)
rc.enqueueReplica(replica)
err = nil
}
}()

// Deprecated and no longer used by Longhorn, but maybe someone's external tooling uses it? Remove in v1.7.0.
replica.Status.EvictionRequested = replica.Spec.EvictionRequested // nolint: staticcheck

return rc.instanceHandler.ReconcileInstanceState(replica, &replica.Spec.InstanceSpec, &replica.Status.InstanceStatus)
}

Check notice on line 303 in controller/replica_controller.go

View check run for this annotation

codefactor.io / CodeFactor

controller/replica_controller.go#L210-L303

Complex Method
func (rc *ReplicaController) enqueueReplica(obj interface{}) {
key, err := controller.KeyFunc(obj)
if err != nil {
Expand All @@ -311,83 +311,83 @@
rc.queue.Add(key)
}

func (rc *ReplicaController) CreateInstance(obj interface{}) (*longhorn.InstanceProcess, error) {
r, ok := obj.(*longhorn.Replica)
if !ok {
return nil, fmt.Errorf("invalid object for replica instance creation: %v", obj)
}

dataPath := types.GetReplicaDataPath(r.Spec.DiskPath, r.Spec.DataDirectoryName)
if r.Spec.NodeID == "" || dataPath == "" || r.Spec.DiskID == "" || r.Spec.VolumeSize == 0 {
return nil, fmt.Errorf("missing parameters for replica instance creation: %v", r)
}

var err error
backingImagePath := ""
if r.Spec.BackingImage != "" {
if backingImagePath, err = rc.GetBackingImagePathForReplicaStarting(r); err != nil {
r.Status.Conditions = types.SetCondition(r.Status.Conditions, longhorn.ReplicaConditionTypeWaitForBackingImage,
longhorn.ConditionStatusTrue, longhorn.ReplicaConditionReasonWaitForBackingImageFailed, err.Error())
return nil, err
}
if backingImagePath == "" {
r.Status.Conditions = types.SetCondition(r.Status.Conditions, longhorn.ReplicaConditionTypeWaitForBackingImage,
longhorn.ConditionStatusTrue, longhorn.ReplicaConditionReasonWaitForBackingImageWaiting, "")
return nil, nil
}
}

r.Status.Conditions = types.SetCondition(r.Status.Conditions, longhorn.ReplicaConditionTypeWaitForBackingImage,
longhorn.ConditionStatusFalse, "", "")

if IsRebuildingReplica(r) {
canStart, err := rc.CanStartRebuildingReplica(r)
if err != nil {
return nil, err
}
if !canStart {
return nil, nil
}
}

im, err := rc.ds.GetInstanceManagerByInstanceRO(obj)
if err != nil {
return nil, err
}

c, err := engineapi.NewInstanceManagerClient(im)
if err != nil {
return nil, err
}
defer c.Close()

v, err := rc.ds.GetVolumeRO(r.Spec.VolumeName)
if err != nil {
return nil, err
}

cliAPIVersion, err := rc.ds.GetDataEngineImageCLIAPIVersion(r.Spec.Image, r.Spec.DataEngine)
if err != nil {
return nil, err
}

diskName, err := rc.getDiskNameFromUUID(r)
if err != nil {
return nil, err
}

return c.ReplicaInstanceCreate(&engineapi.ReplicaInstanceCreateRequest{
Replica: r,
DiskName: diskName,
DataPath: dataPath,
BackingImagePath: backingImagePath,
DataLocality: v.Spec.DataLocality,
ExposeRequired: true,
ImIP: im.Status.IP,
EngineCLIAPIVersion: cliAPIVersion,
})
}

Check notice on line 390 in controller/replica_controller.go

View check run for this annotation

codefactor.io / CodeFactor

controller/replica_controller.go#L314-L390

Complex Method
func (rc *ReplicaController) getDiskNameFromUUID(r *longhorn.Replica) (string, error) {
node, err := rc.ds.GetNodeRO(rc.controllerID)
if err != nil {
Expand Down Expand Up @@ -510,84 +510,84 @@
return true, nil
}

func (rc *ReplicaController) DeleteInstance(obj interface{}) error {
r, ok := obj.(*longhorn.Replica)
if !ok {
return fmt.Errorf("invalid object for replica instance deletion: %v", obj)
}
log := getLoggerForReplica(rc.logger, r)

if types.IsDataEngineV1(r.Spec.DataEngine) {
if err := rc.deleteInstanceWithCLIAPIVersionOne(r); err != nil {
return err
}
}

var im *longhorn.InstanceManager
var err error
// Not assigned or not updated, try best to delete
if r.Status.InstanceManagerName == "" {
if r.Spec.NodeID == "" {
log.Warnf("Replica %v does not set instance manager name and node ID, will skip the actual instance deletion", r.Name)
return nil
}
im, err = rc.ds.GetInstanceManagerByInstance(obj)
if err != nil {
log.WithError(err).Warnf("Failed to detect instance manager for replica %v, will skip the actual instance deletion", r.Name)
return nil
}
log.Infof("Cleaning up the instance for replica %v in instance manager %v", r.Name, im.Name)
} else {
im, err = rc.ds.GetInstanceManager(r.Status.InstanceManagerName)
if err != nil {
if !apierrors.IsNotFound(err) {
return err
}
// The related node may be directly deleted.
log.Warnf("The replica instance manager %v is gone during the replica instance %v deletion. Will do nothing for the deletion", r.Status.InstanceManagerName, r.Name)
return nil
}
}

if im.Status.CurrentState != longhorn.InstanceManagerStateRunning {
return nil
}

c, err := engineapi.NewInstanceManagerClient(im)
if err != nil {
return err
}
defer c.Close()

// No need to delete the instance if the replica is backed by a SPDK lvol
cleanupRequired := false
if canDeleteInstance(r) {
cleanupRequired = true
}

log.Info("Deleting replica instance")
log.WithField("cleanupRequired", cleanupRequired).Infof("Deleting replica instance on disk %v", r.Spec.DiskPath)

err = c.InstanceDelete(r.Spec.DataEngine, r.Name, string(longhorn.InstanceManagerTypeReplica), r.Spec.DiskID, cleanupRequired)
if err != nil && !types.ErrorIsNotFound(err) {
return err
}

if err := deleteUnixSocketFile(r.Spec.VolumeName); err != nil && !types.ErrorIsNotFound(err) {
log.WithError(err).Warnf("Failed to delete unix-domain-socket file for volume %v", r.Spec.VolumeName)
}

// Directly remove the instance from the map. Best effort.
if im.Status.APIVersion == engineapi.IncompatibleInstanceManagerAPIVersion {
delete(im.Status.InstanceReplicas, r.Name)
delete(im.Status.Instances, r.Name) // nolint: staticcheck
if _, err := rc.ds.UpdateInstanceManagerStatus(im); err != nil {
return err
}
}

return nil
}

Check notice on line 590 in controller/replica_controller.go

View check run for this annotation

codefactor.io / CodeFactor

controller/replica_controller.go#L513-L590

Complex Method
func canDeleteInstance(r *longhorn.Replica) bool {
return types.IsDataEngineV1(r.Spec.DataEngine) ||
(types.IsDataEngineV2(r.Spec.DataEngine) && r.DeletionTimestamp != nil)
Expand Down
20 changes: 14 additions & 6 deletions engineapi/instance_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,14 @@ func parseInstance(p *imapi.Instance) *longhorn.InstanceProcess {
DataEngine: getDataEngineFromInstanceProcess(p),
},
Status: longhorn.InstanceProcessStatus{
Type: getTypeForInstance(longhorn.InstanceType(p.Type), p.Name),
State: longhorn.InstanceState(p.InstanceStatus.State),
ErrorMsg: p.InstanceStatus.ErrorMsg,
Conditions: p.InstanceStatus.Conditions,
PortStart: p.InstanceStatus.PortStart,
PortEnd: p.InstanceStatus.PortEnd,
Type: getTypeForInstance(longhorn.InstanceType(p.Type), p.Name),
State: longhorn.InstanceState(p.InstanceStatus.State),
ErrorMsg: p.InstanceStatus.ErrorMsg,
Conditions: p.InstanceStatus.Conditions,
PortStart: p.InstanceStatus.PortStart,
PortEnd: p.InstanceStatus.PortEnd,
TargetPortStart: p.InstanceStatus.TargetPortStart,
TargetPortEnd: p.InstanceStatus.TargetPortEnd,

// FIXME: These fields are not used, maybe we can deprecate them later.
Listen: "",
Expand Down Expand Up @@ -440,6 +442,9 @@ type EngineInstanceCreateRequest struct {
DataLocality longhorn.DataLocality
ImIP string
EngineCLIAPIVersion int
UpgradeRequired bool
InitiatorAddress string
TargetAddress string
}

// EngineInstanceCreate creates a new engine instance
Expand Down Expand Up @@ -494,6 +499,9 @@ func (c *InstanceManagerClient) EngineInstanceCreate(req *EngineInstanceCreateRe
Engine: imclient.EngineCreateRequest{
ReplicaAddressMap: replicaAddresses,
Frontend: frontend,
UpgradeRequired: req.UpgradeRequired,
InitiatorAddress: req.InitiatorAddress,
TargetAddress: req.TargetAddress,
},
})

Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ require (
github.com/jinzhu/copier v0.3.5
github.com/kubernetes-csi/csi-lib-utils v0.6.1
github.com/longhorn/backing-image-manager v1.7.0-dev.0.20240707082104-dfbe0d008b9c
github.com/longhorn/backupstore v0.0.0-20240706152841-78e2c8892f4e
github.com/longhorn/backupstore v0.0.0-20240709004445-1cadf9073de3
github.com/longhorn/go-common-libs v0.0.0-20240707062002-b9354601827e
github.com/longhorn/go-iscsi-helper v0.0.0-20240708025845-7cc78e60866a
github.com/longhorn/go-spdk-helper v0.0.0-20240708060539-de887e9cc6db
github.com/longhorn/longhorn-engine v1.7.0-dev.0.20240707085442-0bfac42c4aff
github.com/longhorn/longhorn-instance-manager v1.7.0-dev.0.20240707105202-1a2fb0b7ea9e
github.com/longhorn/longhorn-instance-manager v1.7.0-dev.0.20240709072210-84ba8e974de0
github.com/longhorn/longhorn-share-manager v1.7.0-dev.0.20240707073010-6d6d282d334a
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.18.0
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1222,8 +1222,8 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/longhorn/backing-image-manager v1.7.0-dev.0.20240707082104-dfbe0d008b9c h1:KkY6aqQB8j9VWxNHNYObsA0Plyg40ZrSp1YSfnFDIUQ=
github.com/longhorn/backing-image-manager v1.7.0-dev.0.20240707082104-dfbe0d008b9c/go.mod h1:ZXD/+yKwMer/eZzwQ2ev/eAyLvih7WNq6NDgfUpvQ+8=
github.com/longhorn/backupstore v0.0.0-20240706152841-78e2c8892f4e h1:UtuNgcxi6YSdlY711WXEa5lHqET6p3QL+4ZkAJnVl9w=
github.com/longhorn/backupstore v0.0.0-20240706152841-78e2c8892f4e/go.mod h1:X9DZ+xqReDQfSsGCxRnNViBxoxSbHezjfejwTPp17NI=
github.com/longhorn/backupstore v0.0.0-20240709004445-1cadf9073de3 h1:DCNyiGtXlYKMdXBm7p3l86pXEaP0klFpes+BtLYleQc=
github.com/longhorn/backupstore v0.0.0-20240709004445-1cadf9073de3/go.mod h1:IJ7rVDB0l5J8YBFgvbYRM0dCF9pLhnIToia+4PDzNqY=
github.com/longhorn/go-common-libs v0.0.0-20240707062002-b9354601827e h1:0SiyvTuovYc9kLJbjagTSxv3sOfCCU9FQJasRo7bgzU=
github.com/longhorn/go-common-libs v0.0.0-20240707062002-b9354601827e/go.mod h1:vX53A9KF4RHC1UTbEGouZHsZO6bwT3zk63l1hvwF5T8=
github.com/longhorn/go-iscsi-helper v0.0.0-20240708025845-7cc78e60866a h1:8FYqfmKkssHYiZqgpnodiyzjPkYmqSViEjXdCvij3rQ=
Expand All @@ -1232,8 +1232,8 @@ github.com/longhorn/go-spdk-helper v0.0.0-20240708060539-de887e9cc6db h1:wQRJNpZ
github.com/longhorn/go-spdk-helper v0.0.0-20240708060539-de887e9cc6db/go.mod h1:BMhlxcEnrn0/jyO+9cMV1gWz2jkmeGFZLnGIqsH3lv0=
github.com/longhorn/longhorn-engine v1.7.0-dev.0.20240707085442-0bfac42c4aff h1:w8z+IYOHtvWEW7jZniVDX1vJGcGr4DIt2UmHeSxzXKU=
github.com/longhorn/longhorn-engine v1.7.0-dev.0.20240707085442-0bfac42c4aff/go.mod h1:u0TZ1221YusDYA+ExdVLjLid1Ps6JuJXgh9185l5D9Y=
github.com/longhorn/longhorn-instance-manager v1.7.0-dev.0.20240707105202-1a2fb0b7ea9e h1:rRgImfGjiuRSA7nm0tbf6vK3j5hVbdHdiQDU5aFnDEo=
github.com/longhorn/longhorn-instance-manager v1.7.0-dev.0.20240707105202-1a2fb0b7ea9e/go.mod h1:EFuyWq4P+esJTCDjJSiTk0vVG5rE4/5Ahq5YWyqoF/c=
github.com/longhorn/longhorn-instance-manager v1.7.0-dev.0.20240709072210-84ba8e974de0 h1:zhLAX9ggz3egAGVLU45m7RAGD/BSxiAG+mJGcO8IH8k=
github.com/longhorn/longhorn-instance-manager v1.7.0-dev.0.20240709072210-84ba8e974de0/go.mod h1:u3rEJikFcEGOlzMcAZjvs0mecBYA5RQITQLVfimFSyE=
github.com/longhorn/longhorn-share-manager v1.7.0-dev.0.20240707073010-6d6d282d334a h1:3CqA8oswld3wmPGS+nNA8IQIo8atzgQNbYSf8MzAe5k=
github.com/longhorn/longhorn-share-manager v1.7.0-dev.0.20240707073010-6d6d282d334a/go.mod h1:R6+NscPU4lAV5ueO7//lBCAO3en0aDbZi5KkkOSUJvk=
github.com/longhorn/types v0.0.0-20240706151541-33cb010c3544 h1:U08l+0SbxCsododsraBHB5PdXrQme3TEh9iaREhRLQs=
Expand Down
18 changes: 18 additions & 0 deletions k8s/crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2115,6 +2115,12 @@ spec:
type: integer
state:
type: string
targetPortEnd:
format: int32
type: integer
targetPortStart:
format: int32
type: integer
type:
type: string
type: object
Expand Down Expand Up @@ -2158,6 +2164,12 @@ spec:
type: integer
state:
type: string
targetPortEnd:
format: int32
type: integer
targetPortStart:
format: int32
type: integer
type:
type: string
type: object
Expand Down Expand Up @@ -2201,6 +2213,12 @@ spec:
type: integer
state:
type: string
targetPortEnd:
format: int32
type: integer
targetPortStart:
format: int32
type: integer
type:
type: string
type: object
Expand Down
4 changes: 4 additions & 0 deletions k8s/pkg/apis/longhorn/v1beta2/instancemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ type InstanceProcessStatus struct {
// +optional
PortStart int32 `json:"portStart"`
// +optional
TargetPortEnd int32 `json:"targetPortEnd"`
// +optional
TargetPortStart int32 `json:"targetPortStart"`
// +optional
State InstanceState `json:"state"`
// +optional
Type InstanceType `json:"type"`
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading