mirror of https://github.com/k3s-io/k3s
Add validation for node creation.
parent
f3ebe30605
commit
49ff04765b
|
@ -37,6 +37,7 @@ import (
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/record"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/record"
|
||||||
|
@ -217,7 +218,11 @@ func startComponents(firstManifestURL, secondManifestURL, apiVersion string) (st
|
||||||
// TODO: Write an integration test for the replication controllers watch.
|
// TODO: Write an integration test for the replication controllers watch.
|
||||||
controllerManager.Run(1 * time.Second)
|
controllerManager.Run(1 * time.Second)
|
||||||
|
|
||||||
nodeResources := &api.NodeResources{}
|
nodeResources := &api.NodeResources{
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
}}
|
||||||
|
|
||||||
nodeController := nodeControllerPkg.NewNodeController(nil, "", machineList, nodeResources, cl, fakeKubeletClient{},
|
nodeController := nodeControllerPkg.NewNodeController(nil, "", machineList, nodeResources, cl, fakeKubeletClient{},
|
||||||
record.FromSource(api.EventSource{Component: "controllermanager"}), 10, 5*time.Minute)
|
record.FromSource(api.EventSource{Component: "controllermanager"}), 10, 5*time.Minute)
|
||||||
|
|
|
@ -909,6 +909,28 @@ func ValidateReadOnlyPersistentDisks(volumes []api.Volume) errs.ValidationErrorL
|
||||||
func ValidateMinion(node *api.Node) errs.ValidationErrorList {
|
func ValidateMinion(node *api.Node) errs.ValidationErrorList {
|
||||||
allErrs := errs.ValidationErrorList{}
|
allErrs := errs.ValidationErrorList{}
|
||||||
allErrs = append(allErrs, ValidateObjectMeta(&node.ObjectMeta, false, ValidateNodeName).Prefix("metadata")...)
|
allErrs = append(allErrs, ValidateObjectMeta(&node.ObjectMeta, false, ValidateNodeName).Prefix("metadata")...)
|
||||||
|
// Capacity is required. Within capacity, memory and cpu resources are required.
|
||||||
|
if len(node.Spec.Capacity) == 0 {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldRequired("spec.Capacity"))
|
||||||
|
} else {
|
||||||
|
if val, ok := node.Spec.Capacity[api.ResourceMemory]; !ok {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldRequired("spec.Capacity[memory]"))
|
||||||
|
} else if val.Value() < 0 {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid("spec.Capacity[memory]", val, "memory capacity cannot be negative"))
|
||||||
|
}
|
||||||
|
if val, ok := node.Spec.Capacity[api.ResourceCPU]; !ok {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldRequired("spec.Capacity[cpu]"))
|
||||||
|
} else if val.Value() < 0 {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid("spec.Capacity[cpu]", val, "cpu capacity cannot be negative"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// external ID is required.
|
||||||
|
if len(node.Spec.ExternalID) == 0 {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldRequired("spec.ExternalID"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(rjnagal): Ignore PodCIDR till its completely implemented.
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1823,6 +1823,14 @@ func TestValidateMinion(t *testing.T) {
|
||||||
{Type: api.NodeLegacyHostIP, Address: "something"},
|
{Type: api.NodeLegacyHostIP, Address: "something"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "external",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
api.ResourceName("my.org/gpu"): resource.MustParse("10"),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ObjectMeta: api.ObjectMeta{
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
@ -1833,6 +1841,13 @@ func TestValidateMinion(t *testing.T) {
|
||||||
{Type: api.NodeLegacyHostIP, Address: "something"},
|
{Type: api.NodeLegacyHostIP, Address: "something"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "external",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("0"),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, successCase := range successCases {
|
for _, successCase := range successCases {
|
||||||
|
@ -1850,12 +1865,97 @@ func TestValidateMinion(t *testing.T) {
|
||||||
Status: api.NodeStatus{
|
Status: api.NodeStatus{
|
||||||
Addresses: []api.NodeAddress{},
|
Addresses: []api.NodeAddress{},
|
||||||
},
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "external",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"invalid-labels": {
|
"invalid-labels": {
|
||||||
ObjectMeta: api.ObjectMeta{
|
ObjectMeta: api.ObjectMeta{
|
||||||
Name: "abc-123",
|
Name: "abc-123",
|
||||||
Labels: invalidSelector,
|
Labels: invalidSelector,
|
||||||
},
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "external",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"missing-external-id": {
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: "abc-123",
|
||||||
|
Labels: validSelector,
|
||||||
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"missing-capacity": {
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: "abc-123",
|
||||||
|
Labels: validSelector,
|
||||||
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "external",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"missing-memory": {
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: "abc-123",
|
||||||
|
Labels: validSelector,
|
||||||
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "external",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"missing-cpu": {
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: "abc-123",
|
||||||
|
Labels: validSelector,
|
||||||
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "external",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-memory": {
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: "abc-123",
|
||||||
|
Labels: validSelector,
|
||||||
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "external",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("-10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"invalid-cpu": {
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: "abc-123",
|
||||||
|
Labels: validSelector,
|
||||||
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "external",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("-10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for k, v := range errorCases {
|
for k, v := range errorCases {
|
||||||
|
@ -1865,10 +1965,17 @@ func TestValidateMinion(t *testing.T) {
|
||||||
}
|
}
|
||||||
for i := range errs {
|
for i := range errs {
|
||||||
field := errs[i].(*errors.ValidationError).Field
|
field := errs[i].(*errors.ValidationError).Field
|
||||||
if field != "metadata.name" &&
|
expectedFields := map[string]bool{
|
||||||
field != "metadata.labels" &&
|
"metadata.name": true,
|
||||||
field != "metadata.annotations" &&
|
"metadata.labels": true,
|
||||||
field != "metadata.namespace" {
|
"metadata.annotations": true,
|
||||||
|
"metadata.namespace": true,
|
||||||
|
"spec.Capacity": true,
|
||||||
|
"spec.Capacity[memory]": true,
|
||||||
|
"spec.Capacity[cpu]": true,
|
||||||
|
"spec.ExternalID": true,
|
||||||
|
}
|
||||||
|
if expectedFields[field] == false {
|
||||||
t.Errorf("%s: missing prefix for: %v", k, errs[i])
|
t.Errorf("%s: missing prefix for: %v", k, errs[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -520,7 +520,9 @@ func (nc *NodeController) GetStaticNodesWithSpec() (*api.NodeList, error) {
|
||||||
for _, nodeID := range nc.nodes {
|
for _, nodeID := range nc.nodes {
|
||||||
node := api.Node{
|
node := api.Node{
|
||||||
ObjectMeta: api.ObjectMeta{Name: nodeID},
|
ObjectMeta: api.ObjectMeta{Name: nodeID},
|
||||||
Spec: api.NodeSpec{Capacity: nc.staticResources.Capacity},
|
Spec: api.NodeSpec{
|
||||||
|
Capacity: nc.staticResources.Capacity,
|
||||||
|
ExternalID: nodeID},
|
||||||
}
|
}
|
||||||
result.Items = append(result.Items, node)
|
result.Items = append(result.Items, node)
|
||||||
}
|
}
|
||||||
|
|
|
@ -279,8 +279,14 @@ func TestCreateGetStaticNodesWithSpec(t *testing.T) {
|
||||||
Items: []api.Node{
|
Items: []api.Node{
|
||||||
{
|
{
|
||||||
ObjectMeta: api.ObjectMeta{Name: "node0"},
|
ObjectMeta: api.ObjectMeta{Name: "node0"},
|
||||||
Spec: api.NodeSpec{},
|
Spec: api.NodeSpec{
|
||||||
Status: api.NodeStatus{},
|
ExternalID: "node0",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: api.NodeStatus{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -291,21 +297,39 @@ func TestCreateGetStaticNodesWithSpec(t *testing.T) {
|
||||||
Items: []api.Node{
|
Items: []api.Node{
|
||||||
{
|
{
|
||||||
ObjectMeta: api.ObjectMeta{Name: "node0"},
|
ObjectMeta: api.ObjectMeta{Name: "node0"},
|
||||||
Spec: api.NodeSpec{},
|
Spec: api.NodeSpec{
|
||||||
Status: api.NodeStatus{},
|
ExternalID: "node0",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: api.NodeStatus{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ObjectMeta: api.ObjectMeta{Name: "node1"},
|
ObjectMeta: api.ObjectMeta{Name: "node1"},
|
||||||
Spec: api.NodeSpec{},
|
Spec: api.NodeSpec{
|
||||||
Status: api.NodeStatus{},
|
ExternalID: "node1",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: api.NodeStatus{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resources := api.NodeResources{
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
}
|
||||||
for _, item := range table {
|
for _, item := range table {
|
||||||
nodeController := NewNodeController(nil, "", item.machines, &api.NodeResources{}, nil, nil, nil, 10, time.Minute)
|
nodeController := NewNodeController(nil, "", item.machines, &resources, nil, nil, nil, 10, time.Minute)
|
||||||
nodes, err := nodeController.GetStaticNodesWithSpec()
|
nodes, err := nodeController.GetStaticNodesWithSpec()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
@ -736,6 +760,13 @@ func TestSyncProbedNodeStatus(t *testing.T) {
|
||||||
{Type: api.NodeLegacyHostIP, Address: "1.2.3.4"},
|
{Type: api.NodeLegacyHostIP, Address: "1.2.3.4"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "node0",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ObjectMeta: api.ObjectMeta{Name: "node1"},
|
ObjectMeta: api.ObjectMeta{Name: "node1"},
|
||||||
|
@ -760,6 +791,13 @@ func TestSyncProbedNodeStatus(t *testing.T) {
|
||||||
{Type: api.NodeLegacyHostIP, Address: "1.2.3.4"},
|
{Type: api.NodeLegacyHostIP, Address: "1.2.3.4"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "node1",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedRequestCount: 3, // List + 2xUpdate
|
expectedRequestCount: 3, // List + 2xUpdate
|
||||||
|
@ -1282,6 +1320,13 @@ func TestMonitorNodeStatusUpdateStatus(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "node0",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Fake: client.Fake{
|
Fake: client.Fake{
|
||||||
|
@ -1306,6 +1351,13 @@ func TestMonitorNodeStatusUpdateStatus(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "node0",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1330,6 +1382,13 @@ func TestMonitorNodeStatusUpdateStatus(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "node0",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Fake: client.Fake{
|
Fake: client.Fake{
|
||||||
|
@ -1357,7 +1416,16 @@ func TestMonitorNodeStatusUpdateStatus(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNode(name string) *api.Node {
|
func newNode(name string) *api.Node {
|
||||||
return &api.Node{ObjectMeta: api.ObjectMeta{Name: name}}
|
return &api.Node{
|
||||||
|
ObjectMeta: api.ObjectMeta{Name: name},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: name,
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("10G"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPod(name, host string) *api.Pod {
|
func newPod(name, host string) *api.Pod {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/rest/resttest"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||||
|
@ -61,6 +62,13 @@ func validNewNode() *api.Node {
|
||||||
"name": "foo",
|
"name": "foo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Spec: api.NodeSpec{
|
||||||
|
ExternalID: "external",
|
||||||
|
Capacity: api.ResourceList{
|
||||||
|
api.ResourceName(api.ResourceCPU): resource.MustParse("10"),
|
||||||
|
api.ResourceName(api.ResourceMemory): resource.MustParse("0"),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -133,6 +133,10 @@ var aMinion string = `
|
||||||
"kind": "Minion",
|
"kind": "Minion",
|
||||||
"apiVersion": "v1beta1",
|
"apiVersion": "v1beta1",
|
||||||
"id": "a",
|
"id": "a",
|
||||||
|
"resources": {
|
||||||
|
"capacity": { "memory": "10", "cpu": "10"}
|
||||||
|
},
|
||||||
|
"externalID": "external",
|
||||||
"hostIP": "10.10.10.10"%s
|
"hostIP": "10.10.10.10"%s
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
Loading…
Reference in New Issue