зеркало из https://github.com/Azure/ARO-RP.git
Backporting old admin portal changes (#2137)
Co-authored-by: Ellis Johnson <elljohns@redhat.com>
This commit is contained in:
Родитель
93663d1d35
Коммит
cf9c5210b4
|
@ -30,6 +30,7 @@ gomock_reflect_*
|
|||
**/*.swp
|
||||
/portal/v1/node_modules/
|
||||
/portal/v2/node_modules/
|
||||
portal/v2/.vscode/
|
||||
.idea*
|
||||
/hack/hive-config/crds
|
||||
/hack/hive-config/hive-deployment.yaml
|
||||
|
|
|
@ -84,9 +84,13 @@ func run(ctx context.Context, l *logrus.Entry) error {
|
|||
r.Use(middleware.Panic(l))
|
||||
|
||||
r.NewRoute().PathPrefix(
|
||||
"/apis/config.openshift.io/v1/clusteroperators",
|
||||
"/api/config.openshift.io/v1/clusteroperators",
|
||||
).HandlerFunc(resp(cluster.MustAsset("clusteroperator.json")))
|
||||
|
||||
r.NewRoute().PathPrefix(
|
||||
"/api/v1/nodes",
|
||||
).HandlerFunc(resp(cluster.MustAsset("nodes.json")))
|
||||
|
||||
s := &http.Server{
|
||||
Handler: frontendmiddleware.Lowercase(r),
|
||||
ReadTimeout: 10 * time.Second,
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -23,6 +23,7 @@ type AdminOpenShiftCluster struct {
|
|||
FailedProvisioningState string `json:"failedprovisioningState"`
|
||||
Version string `json:"version"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
LastModified string `json:"lastModified"`
|
||||
ProvisionedBy string `json:"provisionedBy"`
|
||||
}
|
||||
|
||||
|
@ -58,6 +59,11 @@ func (p *portal) clusters(w http.ResponseWriter, r *http.Request) {
|
|||
createdAt = doc.OpenShiftCluster.Properties.CreatedAt.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
lastModified := "Unknown"
|
||||
if doc.OpenShiftCluster.SystemData.LastModifiedAt != nil {
|
||||
lastModified = doc.OpenShiftCluster.SystemData.LastModifiedAt.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
clusters = append(clusters, &AdminOpenShiftCluster{
|
||||
Key: doc.ID,
|
||||
ResourceId: doc.OpenShiftCluster.ID,
|
||||
|
@ -66,6 +72,7 @@ func (p *portal) clusters(w http.ResponseWriter, r *http.Request) {
|
|||
ResourceGroup: resourceGroup,
|
||||
Version: doc.OpenShiftCluster.Properties.ClusterProfile.Version,
|
||||
CreatedAt: createdAt,
|
||||
LastModified: lastModified,
|
||||
ProvisionedBy: doc.OpenShiftCluster.Properties.ProvisionedBy,
|
||||
ProvisioningState: ps.String(),
|
||||
FailedProvisioningState: fps.String(),
|
||||
|
@ -93,13 +100,88 @@ func (p *portal) clusterOperators(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
res, err := fetcher.ClusterOperators(ctx)
|
||||
clusterOperators, err := fetcher.ClusterOperators(ctx)
|
||||
if err != nil {
|
||||
p.internalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(res, "", " ")
|
||||
b, err := json.MarshalIndent(clusterOperators, "", " ")
|
||||
if err != nil {
|
||||
p.internalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
|
||||
func (p *portal) nodes(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
fetcher, err := p.makeFetcher(ctx, r)
|
||||
if err != nil {
|
||||
p.internalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
nodes, err := fetcher.Nodes(ctx)
|
||||
if err != nil {
|
||||
p.internalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(nodes, "", " ")
|
||||
if err != nil {
|
||||
p.internalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
|
||||
func (p *portal) machines(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
fetcher, err := p.makeFetcher(ctx, r)
|
||||
if err != nil {
|
||||
p.internalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
machines, err := fetcher.Machines(ctx)
|
||||
if err != nil {
|
||||
p.internalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(machines, "", " ")
|
||||
if err != nil {
|
||||
p.internalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(b)
|
||||
}
|
||||
|
||||
func (p *portal) machineSets(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
fetcher, err := p.makeFetcher(ctx, r)
|
||||
if err != nil {
|
||||
p.internalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
machineSets, err := fetcher.MachineSets(ctx)
|
||||
if err != nil {
|
||||
p.internalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(machineSets, "", " ")
|
||||
if err != nil {
|
||||
p.internalServerError(w, err)
|
||||
return
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -43,7 +43,7 @@ func clusterOperatorsInformationFromOperatorList(operators *configv1.ClusterOper
|
|||
}
|
||||
|
||||
func (f *realFetcher) ClusterOperators(ctx context.Context) (*ClusterOperatorsInformation, error) {
|
||||
r, err := f.configcli.ConfigV1().ClusterOperators().List(ctx, metav1.ListOptions{})
|
||||
r, err := f.configCli.ConfigV1().ClusterOperators().List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -28,18 +28,17 @@ func TestClusterOperators(t *testing.T) {
|
|||
t.Error(err)
|
||||
}
|
||||
|
||||
// lol golang
|
||||
converted := make([]kruntime.Object, len(operators.Items))
|
||||
for i := range operators.Items {
|
||||
converted[i] = &operators.Items[i]
|
||||
}
|
||||
|
||||
configcli := configfake.NewSimpleClientset(converted...)
|
||||
configCli := configfake.NewSimpleClientset(converted...)
|
||||
|
||||
_, log := testlog.New()
|
||||
|
||||
rf := &realFetcher{
|
||||
configcli: configcli,
|
||||
configCli: configCli,
|
||||
log: log,
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,9 @@ import (
|
|||
"context"
|
||||
|
||||
configclient "github.com/openshift/client-go/config/clientset/versioned"
|
||||
machineclient "github.com/openshift/client-go/machine/clientset/versioned"
|
||||
"github.com/sirupsen/logrus"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
|
||||
"github.com/Azure/ARO-RP/pkg/api"
|
||||
"github.com/Azure/ARO-RP/pkg/proxy"
|
||||
|
@ -17,7 +19,10 @@ import (
|
|||
// FetchClient is the interface that the Admin Portal Frontend uses to gather
|
||||
// information about clusters. It returns frontend-suitable data structures.
|
||||
type FetchClient interface {
|
||||
Nodes(context.Context) (*NodeListInformation, error)
|
||||
ClusterOperators(context.Context) (*ClusterOperatorsInformation, error)
|
||||
Machines(context.Context) (*MachineListInformation, error)
|
||||
MachineSets(context.Context) (*MachineSetListInformation, error)
|
||||
}
|
||||
|
||||
// client is an implementation of FetchClient. It currently contains a "fetcher"
|
||||
|
@ -36,8 +41,10 @@ type client struct {
|
|||
// contains Kubernetes clients and returns the frontend-suitable data
|
||||
// structures. The concrete implementation of FetchClient wraps this.
|
||||
type realFetcher struct {
|
||||
log *logrus.Entry
|
||||
configcli configclient.Interface
|
||||
log *logrus.Entry
|
||||
configCli configclient.Interface
|
||||
kubernetesCli kubernetes.Interface
|
||||
machineClient machineclient.Interface
|
||||
}
|
||||
|
||||
func newRealFetcher(log *logrus.Entry, dialer proxy.Dialer, doc *api.OpenShiftClusterDocument) (*realFetcher, error) {
|
||||
|
@ -47,14 +54,28 @@ func newRealFetcher(log *logrus.Entry, dialer proxy.Dialer, doc *api.OpenShiftCl
|
|||
return nil, err
|
||||
}
|
||||
|
||||
configcli, err := configclient.NewForConfig(restConfig)
|
||||
configCli, err := configclient.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kubernetesCli, err := kubernetes.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
machineClient, err := machineclient.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &realFetcher{
|
||||
log: log,
|
||||
configcli: configcli,
|
||||
log: log,
|
||||
configCli: configCli,
|
||||
kubernetesCli: kubernetesCli,
|
||||
machineClient: machineClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
package cluster
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
machinev1beta1 "github.com/openshift/api/machine/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type MachinesInformation struct {
|
||||
Name string `json:"name"`
|
||||
CreatedTime string `json:"createdTime"`
|
||||
LastUpdated string `json:"lastUpdated"`
|
||||
ErrorReason string `json:"errorReason"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
LastOperation string `json:"lastOperation"`
|
||||
LastOperationDate string `json:"lastOperationDate"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type MachineListInformation struct {
|
||||
Machines []MachinesInformation `json:"machines"`
|
||||
}
|
||||
|
||||
func MachinesFromMachineList(machines *machinev1beta1.MachineList) *MachineListInformation {
|
||||
final := &MachineListInformation{
|
||||
Machines: make([]MachinesInformation, len(machines.Items)),
|
||||
}
|
||||
|
||||
for i, machine := range machines.Items {
|
||||
machinesInformation := MachinesInformation{
|
||||
Name: machine.Name,
|
||||
CreatedTime: machine.CreationTimestamp.String(),
|
||||
LastUpdated: machine.Status.LastUpdated.String(),
|
||||
ErrorReason: getErrorReason(machine),
|
||||
ErrorMessage: getErrorMessage(machine),
|
||||
LastOperation: getLastOperation(machine),
|
||||
LastOperationDate: getLastOperationDate(machine),
|
||||
Status: getStatus(machine),
|
||||
}
|
||||
final.Machines[i] = machinesInformation
|
||||
}
|
||||
return final
|
||||
}
|
||||
|
||||
func (f *realFetcher) Machines(ctx context.Context) (*MachineListInformation, error) {
|
||||
r, err := f.machineClient.MachineV1beta1().Machines("").List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return MachinesFromMachineList(r), nil
|
||||
}
|
||||
|
||||
func (c *client) Machines(ctx context.Context) (*MachineListInformation, error) {
|
||||
return c.fetcher.Machines(ctx)
|
||||
}
|
||||
|
||||
// Helper Functions
|
||||
func getLastOperation(machine machinev1beta1.Machine) string {
|
||||
lastOperation := "Unknown"
|
||||
if machine.Status.LastOperation != nil &&
|
||||
machine.Status.LastOperation.Description != nil {
|
||||
lastOperation = *machine.Status.LastOperation.Description
|
||||
}
|
||||
return lastOperation
|
||||
}
|
||||
|
||||
func getLastOperationDate(machine machinev1beta1.Machine) string {
|
||||
lastOperationDate := "Unknown"
|
||||
if machine.Status.LastOperation != nil &&
|
||||
machine.Status.LastOperation.LastUpdated != nil {
|
||||
lastOperationDate = machine.Status.LastOperation.LastUpdated.String()
|
||||
}
|
||||
return lastOperationDate
|
||||
}
|
||||
|
||||
func getStatus(machine machinev1beta1.Machine) string {
|
||||
status := "Unknown"
|
||||
if machine.Status.Phase != nil {
|
||||
status = *machine.Status.Phase
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func getErrorReason(machine machinev1beta1.Machine) string {
|
||||
errorReason := "None"
|
||||
if machine.Status.ErrorReason != nil {
|
||||
errorReason = string(*machine.Status.ErrorReason)
|
||||
}
|
||||
return errorReason
|
||||
}
|
||||
|
||||
func getErrorMessage(machine machinev1beta1.Machine) string {
|
||||
errorMessage := "None"
|
||||
if machine.Status.ErrorMessage != nil {
|
||||
errorMessage = *machine.Status.ErrorMessage
|
||||
}
|
||||
return errorMessage
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package cluster
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
machinev1beta1 "github.com/openshift/api/machine/v1beta1"
|
||||
machinefake "github.com/openshift/client-go/machine/clientset/versioned/fake"
|
||||
kruntime "k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
testlog "github.com/Azure/ARO-RP/test/util/log"
|
||||
)
|
||||
|
||||
func TestMachines(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
txt, _ := machinesJsonBytes()
|
||||
|
||||
var machines machinev1beta1.MachineList
|
||||
err := json.Unmarshal(txt, &machines)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
converted := make([]kruntime.Object, len(machines.Items))
|
||||
for i := range machines.Items {
|
||||
converted[i] = &machines.Items[i]
|
||||
}
|
||||
|
||||
machineClient := machinefake.NewSimpleClientset(converted...)
|
||||
|
||||
_, log := testlog.New()
|
||||
|
||||
rf := &realFetcher{
|
||||
machineClient: machineClient,
|
||||
log: log,
|
||||
}
|
||||
|
||||
c := &client{fetcher: rf, log: log}
|
||||
|
||||
info, err := c.Machines(ctx)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
expected := &MachineListInformation{
|
||||
Machines: []MachinesInformation{
|
||||
{
|
||||
Name: "aro-v4-shared-gxqb4-master-0",
|
||||
LastOperation: "Update",
|
||||
Status: "Running",
|
||||
ErrorReason: "None",
|
||||
ErrorMessage: "None",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sort.SliceStable(info.Machines, func(i, j int) bool { return info.Machines[i].Name < info.Machines[j].Name })
|
||||
sort.SliceStable(expected.Machines, func(i, j int) bool { return expected.Machines[i].Name < expected.Machines[j].Name })
|
||||
|
||||
for i, machine := range info.Machines {
|
||||
if machine.CreatedTime == "" {
|
||||
t.Fatal("Node field CreatedTime was null, expected not null")
|
||||
}
|
||||
info.Machines[i].CreatedTime = ""
|
||||
|
||||
if machine.LastUpdated == "" {
|
||||
t.Fatal("Machine field LastUpdated was null, expected not null")
|
||||
}
|
||||
info.Machines[i].LastUpdated = ""
|
||||
|
||||
if machine.LastOperationDate == "" {
|
||||
t.Fatal("Node field LastOperationDate was null, expected not null")
|
||||
}
|
||||
|
||||
dateRegex := regexp.MustCompile(`\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [\+-]\d{4} \w+`)
|
||||
|
||||
if !dateRegex.Match([]byte(machine.LastOperationDate)) {
|
||||
expDateFormat := "2021-08-10T12:21:47 +1000 AEST"
|
||||
|
||||
t.Fatalf("Node field LastOperationDate was in incorrect format %v, expected format of %v",
|
||||
machine.LastOperationDate, expDateFormat)
|
||||
}
|
||||
info.Machines[i].LastOperationDate = ""
|
||||
}
|
||||
|
||||
// No need to check every single machine
|
||||
for _, r := range deep.Equal(expected.Machines[0], info.Machines[0]) {
|
||||
t.Fatal(r)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package cluster
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
machinev1beta1 "github.com/openshift/api/machine/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type OsDiskManagedDisk struct {
|
||||
StorageAccountType string `json:"storageaccounttype"`
|
||||
}
|
||||
type MachineSetProviderSpecValueOSDisk struct {
|
||||
DiskSizeGB int `json:"disksizegb"`
|
||||
OsType string `json:"ostype"`
|
||||
ManagedDisk OsDiskManagedDisk `json:"manageddisk"`
|
||||
}
|
||||
type MachineSetProviderSpecValue struct {
|
||||
Kind string `json:"kind"`
|
||||
Location string `json:"location"`
|
||||
NetworkResourceGroup string `json:"networkresourcegroup"`
|
||||
OsDisk MachineSetProviderSpecValueOSDisk `json:"osdisk"`
|
||||
PublicIP bool `json:"publicip"`
|
||||
PublicLoadBalancer string `json:"publicloadbalancer"`
|
||||
Subnet string `json:"subnet"`
|
||||
VmSize string `json:"vmsize"`
|
||||
Vnet string `json:"vnet"`
|
||||
}
|
||||
|
||||
type MachineSetsInformation struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
CreatedAt string `json:"createdat"`
|
||||
DesiredReplicas int `json:"desiredreplicas"`
|
||||
Replicas int `json:"replicas"`
|
||||
ErrorReason string `json:"errorreason"`
|
||||
ErrorMessage string `json:"errormessage"`
|
||||
PublicLoadBalancerName string `json:"publicloadbalancername"`
|
||||
VMSize string `json:"vmsize"`
|
||||
OSDiskAccountStorageType string `json:"accountstoragetype"`
|
||||
Subnet string `json:"subnet"`
|
||||
VNet string `json:"vnet"`
|
||||
}
|
||||
type MachineSetListInformation struct {
|
||||
MachineSets []MachineSetsInformation `json:"machines"`
|
||||
}
|
||||
|
||||
func (f *realFetcher) MachineSetsFromMachineSetList(ctx context.Context, machineSets *machinev1beta1.MachineSetList) *MachineSetListInformation {
|
||||
final := &MachineSetListInformation{
|
||||
MachineSets: make([]MachineSetsInformation, 0, len(machineSets.Items)),
|
||||
}
|
||||
|
||||
for _, machineSet := range machineSets.Items {
|
||||
var machineSetProviderSpecValue MachineSetProviderSpecValue
|
||||
machineSetJson, err := machineSet.Spec.Template.Spec.ProviderSpec.Value.MarshalJSON()
|
||||
if err != nil {
|
||||
f.log.Logger.Error(err.Error())
|
||||
}
|
||||
json.Unmarshal(machineSetJson, &machineSetProviderSpecValue)
|
||||
|
||||
final.MachineSets = append(final.MachineSets, MachineSetsInformation{
|
||||
Name: machineSet.Name,
|
||||
Type: machineSet.ObjectMeta.Labels["machine.openshift.io/cluster-api-machine-type"],
|
||||
CreatedAt: machineSet.ObjectMeta.CreationTimestamp.String(),
|
||||
DesiredReplicas: int(*machineSet.Spec.Replicas),
|
||||
Replicas: int(machineSet.Status.Replicas),
|
||||
ErrorReason: getErrorReasonMachineSet(machineSet),
|
||||
ErrorMessage: getErrorMessageMachineSet(machineSet),
|
||||
PublicLoadBalancerName: machineSetProviderSpecValue.PublicLoadBalancer,
|
||||
OSDiskAccountStorageType: machineSetProviderSpecValue.OsDisk.ManagedDisk.StorageAccountType,
|
||||
VNet: machineSetProviderSpecValue.Vnet,
|
||||
Subnet: machineSetProviderSpecValue.Subnet,
|
||||
VMSize: machineSetProviderSpecValue.VmSize,
|
||||
})
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
||||
|
||||
func (f *realFetcher) MachineSets(ctx context.Context) (*MachineSetListInformation, error) {
|
||||
r, err := f.machineClient.MachineV1beta1().MachineSets("").List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return f.MachineSetsFromMachineSetList(ctx, r), nil
|
||||
}
|
||||
|
||||
func (c *client) MachineSets(ctx context.Context) (*MachineSetListInformation, error) {
|
||||
return c.fetcher.MachineSets(ctx)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func getErrorMessageMachineSet(machineSet machinev1beta1.MachineSet) string {
|
||||
errorMessage := "None"
|
||||
if machineSet.Status.ErrorMessage != nil {
|
||||
errorMessage = *machineSet.Status.ErrorMessage
|
||||
}
|
||||
return errorMessage
|
||||
}
|
||||
|
||||
func getErrorReasonMachineSet(machineSet machinev1beta1.MachineSet) string {
|
||||
errorReason := "None"
|
||||
if machineSet.Status.ErrorReason != nil {
|
||||
errorReason = string(*machineSet.Status.ErrorReason)
|
||||
}
|
||||
return errorReason
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package cluster
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
machinev1beta1 "github.com/openshift/api/machine/v1beta1"
|
||||
machinefake "github.com/openshift/client-go/machine/clientset/versioned/fake"
|
||||
kruntime "k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
testlog "github.com/Azure/ARO-RP/test/util/log"
|
||||
)
|
||||
|
||||
func TestMachineSets(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
txt, _ := machinesetsJsonBytes()
|
||||
|
||||
var machineSets machinev1beta1.MachineSetList
|
||||
err := json.Unmarshal(txt, &machineSets)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
converted := make([]kruntime.Object, len(machineSets.Items))
|
||||
for i := range machineSets.Items {
|
||||
converted[i] = &machineSets.Items[i]
|
||||
}
|
||||
|
||||
machineClient := machinefake.NewSimpleClientset(converted...)
|
||||
|
||||
_, log := testlog.New()
|
||||
|
||||
rf := &realFetcher{
|
||||
machineClient: machineClient,
|
||||
log: log,
|
||||
}
|
||||
|
||||
c := &client{fetcher: rf, log: log}
|
||||
|
||||
info, err := c.MachineSets(ctx)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
for i, machineSet := range machineSets.Items {
|
||||
if i >= len(info.MachineSets) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
info.MachineSets[i].CreatedAt = machineSet.CreationTimestamp.In(time.UTC).String()
|
||||
}
|
||||
|
||||
expected := &MachineSetListInformation{
|
||||
MachineSets: []MachineSetsInformation{
|
||||
{
|
||||
Name: "aro-v4-shared-gxqb4-infra-eastus1",
|
||||
Type: "infra",
|
||||
CreatedAt: "2021-03-09 13:48:16 +0000 UTC",
|
||||
DesiredReplicas: 0,
|
||||
Replicas: 0,
|
||||
ErrorReason: "None",
|
||||
ErrorMessage: "None",
|
||||
PublicLoadBalancerName: "aro-v4-shared-gxqb4",
|
||||
OSDiskAccountStorageType: "Premium_LRS",
|
||||
VNet: "vnet",
|
||||
Subnet: "worker-subnet",
|
||||
VMSize: "Standard_D4s_v3",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sort.SliceStable(info.MachineSets, func(i, j int) bool { return info.MachineSets[i].Replicas < info.MachineSets[j].Replicas })
|
||||
sort.SliceStable(expected.MachineSets, func(i, j int) bool { return expected.MachineSets[i].Replicas < expected.MachineSets[j].Replicas })
|
||||
|
||||
// No need to check every single machine
|
||||
for _, r := range deep.Equal(expected.MachineSets[0], info.MachineSets[0]) {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
package cluster
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type NodeConditions struct {
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
LastHeartbeatTime string `json:"lastHeartbeatTime,omitempty"`
|
||||
LastTransitionTime string `json:"lastTransitionTime,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type Taint struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Effect string `json:"effect"`
|
||||
TimeAdded string `json:"timeAdded,omitempty"`
|
||||
}
|
||||
|
||||
type MachineResources struct {
|
||||
CPU string
|
||||
StorageVolume string
|
||||
Memory string
|
||||
Pods string
|
||||
}
|
||||
|
||||
type Volume struct {
|
||||
Name string
|
||||
Path string
|
||||
}
|
||||
|
||||
type NodeInformation struct {
|
||||
Name string `json:"name"`
|
||||
CreatedTime string `json:"createdTime"`
|
||||
Capacity MachineResources `json:"capacity"`
|
||||
Volumes []Volume `json:"volumes"`
|
||||
Allocatable MachineResources `json:"allocatable"`
|
||||
Taints []Taint `json:"taints"`
|
||||
Conditions []NodeConditions `json:"conditions"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
}
|
||||
|
||||
type NodeListInformation struct {
|
||||
Nodes []NodeInformation `json:"nodes"`
|
||||
}
|
||||
|
||||
func NodesFromNodeList(nodes *corev1.NodeList) *NodeListInformation {
|
||||
final := &NodeListInformation{
|
||||
Nodes: make([]NodeInformation, len(nodes.Items)),
|
||||
}
|
||||
|
||||
for i, node := range nodes.Items {
|
||||
nodeInformation := NodeInformation{
|
||||
Name: node.Name,
|
||||
CreatedTime: node.CreationTimestamp.String(),
|
||||
Capacity: MachineResources{
|
||||
CPU: node.Status.Capacity.Cpu().String(),
|
||||
StorageVolume: node.Status.Capacity.StorageEphemeral().String(),
|
||||
Memory: node.Status.Capacity.Memory().String(),
|
||||
Pods: node.Status.Capacity.Pods().String(),
|
||||
},
|
||||
Allocatable: MachineResources{
|
||||
CPU: node.Status.Allocatable.Cpu().String(),
|
||||
StorageVolume: node.Status.Allocatable.StorageEphemeral().String(),
|
||||
Memory: node.Status.Allocatable.Memory().String(),
|
||||
Pods: node.Status.Allocatable.Pods().String(),
|
||||
},
|
||||
Taints: getTaints(node),
|
||||
Conditions: getNodeConditions(node),
|
||||
Volumes: getVolumes(node),
|
||||
Labels: node.Labels,
|
||||
Annotations: node.Annotations,
|
||||
}
|
||||
final.Nodes[i] = nodeInformation
|
||||
}
|
||||
|
||||
return final
|
||||
}
|
||||
|
||||
func (f *realFetcher) Nodes(ctx context.Context) (*NodeListInformation, error) {
|
||||
r, err := f.kubernetesCli.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NodesFromNodeList(r), nil
|
||||
}
|
||||
|
||||
func (c *client) Nodes(ctx context.Context) (*NodeListInformation, error) {
|
||||
return c.fetcher.Nodes(ctx)
|
||||
}
|
||||
|
||||
// Helping Functions
|
||||
func getNodeConditions(node corev1.Node) []NodeConditions {
|
||||
conditions := []NodeConditions{}
|
||||
for _, condition := range node.Status.Conditions {
|
||||
conditions = append(conditions, NodeConditions{
|
||||
Type: string(condition.Type),
|
||||
Status: string(condition.Status),
|
||||
LastHeartbeatTime: condition.LastHeartbeatTime.String(),
|
||||
LastTransitionTime: condition.LastTransitionTime.String(),
|
||||
Reason: condition.Reason,
|
||||
Message: condition.Message,
|
||||
})
|
||||
}
|
||||
return conditions
|
||||
}
|
||||
|
||||
func getTaints(node corev1.Node) []Taint {
|
||||
taints := []Taint{}
|
||||
for _, taint := range node.Spec.Taints {
|
||||
timeAdded := ""
|
||||
if taint.TimeAdded != nil {
|
||||
timeAdded = taint.TimeAdded.String()
|
||||
}
|
||||
taints = append(taints, Taint{
|
||||
Key: taint.Key,
|
||||
Value: taint.Value,
|
||||
Effect: string(taint.Effect),
|
||||
TimeAdded: timeAdded,
|
||||
})
|
||||
}
|
||||
return taints
|
||||
}
|
||||
|
||||
func getVolumes(node corev1.Node) []Volume {
|
||||
volumes := []Volume{}
|
||||
for _, volume := range node.Status.VolumesAttached {
|
||||
volumes = append(volumes, Volume{
|
||||
Name: string(volume.Name),
|
||||
Path: volume.DevicePath,
|
||||
})
|
||||
}
|
||||
return volumes
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
package cluster
|
||||
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the Apache License 2.0.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kruntime "k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
testlog "github.com/Azure/ARO-RP/test/util/log"
|
||||
)
|
||||
|
||||
func TestNodes(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
txt, _ := nodesJsonBytes()
|
||||
|
||||
var nodes corev1.NodeList
|
||||
err := json.Unmarshal(txt, &nodes)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
converted := make([]kruntime.Object, len(nodes.Items))
|
||||
for i := range nodes.Items {
|
||||
converted[i] = &nodes.Items[i]
|
||||
}
|
||||
|
||||
kubernetes := fake.NewSimpleClientset(converted...)
|
||||
|
||||
_, log := testlog.New()
|
||||
|
||||
rf := &realFetcher{
|
||||
kubernetesCli: kubernetes,
|
||||
log: log,
|
||||
}
|
||||
|
||||
c := &client{fetcher: rf, log: log}
|
||||
|
||||
info, err := c.Nodes(ctx)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
expected := &NodeListInformation{
|
||||
Nodes: []NodeInformation{
|
||||
{
|
||||
Name: "aro-master-0",
|
||||
Capacity: MachineResources{
|
||||
CPU: "8",
|
||||
StorageVolume: "1073189868Ki",
|
||||
Memory: "32933416Ki",
|
||||
Pods: "250",
|
||||
},
|
||||
Allocatable: MachineResources{
|
||||
CPU: "7500m",
|
||||
StorageVolume: "987978038888",
|
||||
Memory: "31782440Ki",
|
||||
Pods: "250",
|
||||
},
|
||||
Taints: []Taint{
|
||||
{
|
||||
Key: "node-role.kubernetes.io/master",
|
||||
Value: "",
|
||||
Effect: "NoSchedule",
|
||||
TimeAdded: "",
|
||||
},
|
||||
},
|
||||
Conditions: []NodeConditions{
|
||||
{
|
||||
Type: "MemoryPressure",
|
||||
Status: "False",
|
||||
Reason: "KubeletHasSufficientMemory",
|
||||
Message: "kubelet has sufficient memory available",
|
||||
},
|
||||
{
|
||||
Type: "DiskPressure",
|
||||
Status: "False",
|
||||
Reason: "KubeletHasNoDiskPressure",
|
||||
Message: "kubelet has no disk pressure",
|
||||
},
|
||||
{
|
||||
Type: "PIDPressure",
|
||||
Status: "False",
|
||||
Reason: "KubeletHasSufficientPID",
|
||||
Message: "kubelet has sufficient PID available",
|
||||
},
|
||||
{
|
||||
Type: "Ready",
|
||||
Status: "True",
|
||||
Reason: "KubeletReady",
|
||||
Message: "kubelet is posting ready status",
|
||||
},
|
||||
},
|
||||
Volumes: make([]Volume, 0),
|
||||
Labels: map[string]string{"beta.kubernetes.io/arch": "amd64",
|
||||
"beta.kubernetes.io/instance-type": "Standard_D8s_v3",
|
||||
"beta.kubernetes.io/os": "linux",
|
||||
"failure-domain.beta.kubernetes.io/region": "eastus",
|
||||
"failure-domain.beta.kubernetes.io/zone": "eastus-1",
|
||||
"kubernetes.io/arch": "amd64",
|
||||
"kubernetes.io/hostname": "aro-master-0",
|
||||
"kubernetes.io/os": "linux",
|
||||
"node-role.kubernetes.io/master": "",
|
||||
"node.kubernetes.io/instance-type": "Standard_D8s_v3",
|
||||
"node.openshift.io/os_id": "rhcos",
|
||||
"topology.kubernetes.io/region": "eastus",
|
||||
"topology.kubernetes.io/zone": "eastus-1"},
|
||||
Annotations: map[string]string{"machine.openshift.io/machine": "openshift-machine-api/aro-master-0",
|
||||
"machineconfiguration.openshift.io/currentConfig": "rendered-master-ebd6f663e22984bdce9081039a6f01c0",
|
||||
"machineconfiguration.openshift.io/desiredConfig": "rendered-master-ebd6f663e22984bdce9081039a6f01c0",
|
||||
"machineconfiguration.openshift.io/reason": "",
|
||||
"machineconfiguration.openshift.io/state": "Done",
|
||||
"volumes.kubernetes.io/controller-managed-attach-detach": "true"},
|
||||
}}}
|
||||
|
||||
sort.SliceStable(info.Nodes, func(i, j int) bool { return info.Nodes[i].Name < info.Nodes[j].Name })
|
||||
sort.SliceStable(expected.Nodes, func(i, j int) bool { return expected.Nodes[i].Name < expected.Nodes[j].Name })
|
||||
|
||||
for i, node := range info.Nodes {
|
||||
dateRegex := regexp.MustCompile(`\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [\+-]\d{4} \w+`)
|
||||
if node.CreatedTime == "" {
|
||||
t.Fatal("Node field CreatedTime was null, expected not null")
|
||||
}
|
||||
|
||||
if !dateRegex.Match([]byte(node.CreatedTime)) {
|
||||
expDateFormat := "2021-08-10 12:21:47 +1000 AEST"
|
||||
t.Fatalf("Node field CreatedTime was in incorrect format %v, expected format of %v",
|
||||
node.CreatedTime, expDateFormat)
|
||||
}
|
||||
info.Nodes[i].CreatedTime = ""
|
||||
|
||||
for j, condition := range node.Conditions {
|
||||
if condition.LastHeartbeatTime == "" {
|
||||
t.Error("Node field LastHeartbeatTime was null, expected not null")
|
||||
}
|
||||
|
||||
if !dateRegex.Match([]byte(condition.LastHeartbeatTime)) {
|
||||
expDateFormat := "2021-08-10 12:21:47 +1000 AEST"
|
||||
t.Fatalf("Node field LastHeartbeatTime was in incorrect format %v, expected format of %v",
|
||||
condition.LastHeartbeatTime, expDateFormat)
|
||||
}
|
||||
info.Nodes[i].Conditions[j].LastHeartbeatTime = ""
|
||||
|
||||
if condition.LastTransitionTime == "" {
|
||||
t.Fatal("Node field LastTransitionTime was null, expected not null")
|
||||
}
|
||||
|
||||
if !dateRegex.Match([]byte(condition.LastTransitionTime)) {
|
||||
expDateFormat := "2021-08-10 12:21:47 +1000 AEST"
|
||||
t.Fatalf("Node field LastTransitionTime was in incorrect format %v, expected format of %v",
|
||||
condition.LastTransitionTime, expDateFormat)
|
||||
}
|
||||
info.Nodes[i].Conditions[j].LastTransitionTime = ""
|
||||
}
|
||||
}
|
||||
|
||||
// No need to check every single node
|
||||
for _, r := range deep.Equal(expected.Nodes[0], info.Nodes[0]) {
|
||||
t.Error(r)
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,725 @@
|
|||
{
|
||||
"apiVersion": "machine.openshift.io/v1beta1",
|
||||
"items": [
|
||||
{
|
||||
"apiVersion": "machine.openshift.io/v1beta1",
|
||||
"kind": "MachineSet",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"machine.openshift.io/GPU": "0",
|
||||
"machine.openshift.io/memoryMb": "16384",
|
||||
"machine.openshift.io/vCPU": "4"
|
||||
},
|
||||
"creationTimestamp": "2021-03-09T13:48:16Z",
|
||||
"generation": 2,
|
||||
"labels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machine-role": "infra",
|
||||
"machine.openshift.io/cluster-api-machine-type": "infra"
|
||||
},
|
||||
"managedFields": [
|
||||
{
|
||||
"apiVersion": "machine.openshift.io/v1beta1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:metadata": {
|
||||
"f:annotations": {
|
||||
".": {},
|
||||
"f:machine.openshift.io/GPU": {},
|
||||
"f:machine.openshift.io/memoryMb": {},
|
||||
"f:machine.openshift.io/vCPU": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"manager": "machine-controller-manager",
|
||||
"operation": "Update",
|
||||
"time": "2021-03-09T13:48:16Z"
|
||||
},
|
||||
{
|
||||
"apiVersion": "machine.openshift.io/v1beta1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:metadata": {
|
||||
"f:labels": {
|
||||
".": {},
|
||||
"f:machine.openshift.io/cluster-api-cluster": {},
|
||||
"f:machine.openshift.io/cluster-api-machine-role": {},
|
||||
"f:machine.openshift.io/cluster-api-machine-type": {}
|
||||
}
|
||||
},
|
||||
"f:spec": {
|
||||
".": {},
|
||||
"f:replicas": {},
|
||||
"f:selector": {
|
||||
".": {},
|
||||
"f:matchLabels": {
|
||||
".": {},
|
||||
"f:machine.openshift.io/cluster-api-cluster": {},
|
||||
"f:machine.openshift.io/cluster-api-machineset": {}
|
||||
}
|
||||
},
|
||||
"f:template": {
|
||||
".": {},
|
||||
"f:metadata": {
|
||||
".": {},
|
||||
"f:labels": {
|
||||
".": {},
|
||||
"f:machine.openshift.io/cluster-api-cluster": {},
|
||||
"f:machine.openshift.io/cluster-api-machine-role": {},
|
||||
"f:machine.openshift.io/cluster-api-machine-type": {},
|
||||
"f:machine.openshift.io/cluster-api-machineset": {}
|
||||
}
|
||||
},
|
||||
"f:spec": {
|
||||
".": {},
|
||||
"f:metadata": {
|
||||
".": {},
|
||||
"f:labels": {
|
||||
".": {},
|
||||
"f:node-role.kubernetes.io/infra": {}
|
||||
}
|
||||
},
|
||||
"f:providerSpec": {
|
||||
".": {},
|
||||
"f:value": {
|
||||
".": {},
|
||||
"f:apiVersion": {},
|
||||
"f:credentialsSecret": {},
|
||||
"f:image": {},
|
||||
"f:kind": {},
|
||||
"f:location": {},
|
||||
"f:metadata": {},
|
||||
"f:networkResourceGroup": {},
|
||||
"f:osDisk": {},
|
||||
"f:publicIP": {},
|
||||
"f:publicLoadBalancer": {},
|
||||
"f:resourceGroup": {},
|
||||
"f:subnet": {},
|
||||
"f:userDataSecret": {},
|
||||
"f:vmSize": {},
|
||||
"f:vnet": {},
|
||||
"f:zone": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"manager": "oc",
|
||||
"operation": "Update",
|
||||
"time": "2021-03-09T13:48:16Z"
|
||||
},
|
||||
{
|
||||
"apiVersion": "machine.openshift.io/v1beta1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:status": {
|
||||
".": {},
|
||||
"f:observedGeneration": {},
|
||||
"f:replicas": {}
|
||||
}
|
||||
},
|
||||
"manager": "machineset-controller",
|
||||
"operation": "Update",
|
||||
"time": "2021-08-11T15:57:21Z"
|
||||
}
|
||||
],
|
||||
"name": "aro-v4-shared-gxqb4-infra-eastus1",
|
||||
"namespace": "openshift-machine-api",
|
||||
"resourceVersion": "189387124",
|
||||
"selfLink": "/apis/machine.openshift.io/v1beta1/namespaces/openshift-machine-api/machinesets/aro-v4-shared-gxqb4-infra-eastus1",
|
||||
"uid": "3b7a3ea9-6709-4091-ac3e-0187fca4f0b3"
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 0,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machineset": "aro-v4-shared-gxqb4-infra-eastus1"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machine-role": "infra",
|
||||
"machine.openshift.io/cluster-api-machine-type": "infra",
|
||||
"machine.openshift.io/cluster-api-machineset": "aro-v4-shared-gxqb4-infra-eastus1"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"node-role.kubernetes.io/infra": ""
|
||||
}
|
||||
},
|
||||
"providerSpec": {
|
||||
"value": {
|
||||
"apiVersion": "azureproviderconfig.openshift.io/v1beta1",
|
||||
"credentialsSecret": {
|
||||
"name": "azure-cloud-credentials",
|
||||
"namespace": "openshift-machine-api"
|
||||
},
|
||||
"image": {
|
||||
"offer": "aro4",
|
||||
"publisher": "azureopenshift",
|
||||
"resourceID": "",
|
||||
"sku": "aro_43",
|
||||
"version": "43.81.20200311"
|
||||
},
|
||||
"kind": "AzureMachineProviderSpec",
|
||||
"location": "eastus",
|
||||
"metadata": {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"networkResourceGroup": "aro-v4-shared",
|
||||
"osDisk": {
|
||||
"diskSizeGB": 128,
|
||||
"managedDisk": {
|
||||
"storageAccountType": "Premium_LRS"
|
||||
},
|
||||
"osType": "Linux"
|
||||
},
|
||||
"publicIP": false,
|
||||
"publicLoadBalancer": "aro-v4-shared-gxqb4",
|
||||
"resourceGroup": "aro-v4-shared-cluster",
|
||||
"subnet": "worker-subnet",
|
||||
"userDataSecret": {
|
||||
"name": "worker-user-data"
|
||||
},
|
||||
"vmSize": "Standard_D4s_v3",
|
||||
"vnet": "vnet",
|
||||
"zone": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"observedGeneration": 2,
|
||||
"replicas": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "machine.openshift.io/v1beta1",
|
||||
"kind": "MachineSet",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"machine.openshift.io/GPU": "0",
|
||||
"machine.openshift.io/memoryMb": "16384",
|
||||
"machine.openshift.io/vCPU": "4"
|
||||
},
|
||||
"creationTimestamp": "2021-08-04T21:02:00Z",
|
||||
"generation": 25,
|
||||
"labels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machine-role": "worker",
|
||||
"machine.openshift.io/cluster-api-machine-type": "worker"
|
||||
},
|
||||
"managedFields": [
|
||||
{
|
||||
"apiVersion": "machine.openshift.io/v1beta1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:metadata": {
|
||||
"f:labels": {
|
||||
".": {},
|
||||
"f:machine.openshift.io/cluster-api-cluster": {}
|
||||
}
|
||||
},
|
||||
"f:spec": {
|
||||
".": {},
|
||||
"f:selector": {
|
||||
".": {},
|
||||
"f:matchLabels": {
|
||||
".": {},
|
||||
"f:machine.openshift.io/cluster-api-cluster": {},
|
||||
"f:machine.openshift.io/cluster-api-machineset": {}
|
||||
}
|
||||
},
|
||||
"f:template": {
|
||||
".": {},
|
||||
"f:metadata": {
|
||||
".": {},
|
||||
"f:labels": {
|
||||
".": {},
|
||||
"f:machine.openshift.io/cluster-api-cluster": {},
|
||||
"f:machine.openshift.io/cluster-api-machineset": {}
|
||||
}
|
||||
},
|
||||
"f:spec": {
|
||||
".": {},
|
||||
"f:metadata": {
|
||||
".": {},
|
||||
"f:labels": {
|
||||
".": {},
|
||||
"f:node-role.kubernetes.io/worker": {},
|
||||
"f:spot": {}
|
||||
}
|
||||
},
|
||||
"f:providerSpec": {
|
||||
".": {},
|
||||
"f:value": {
|
||||
".": {},
|
||||
"f:apiVersion": {},
|
||||
"f:credentialsSecret": {},
|
||||
"f:kind": {},
|
||||
"f:metadata": {},
|
||||
"f:osDisk": {},
|
||||
"f:publicIP": {},
|
||||
"f:spotVMOptions": {},
|
||||
"f:sshPublicKey": {},
|
||||
"f:userDataSecret": {},
|
||||
"f:zone": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"manager": "oc",
|
||||
"operation": "Update",
|
||||
"time": "2021-08-04T21:02:00Z"
|
||||
},
|
||||
{
|
||||
"apiVersion": "machine.openshift.io/v1beta1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:metadata": {
|
||||
"f:annotations": {
|
||||
".": {},
|
||||
"f:machine.openshift.io/GPU": {},
|
||||
"f:machine.openshift.io/memoryMb": {},
|
||||
"f:machine.openshift.io/vCPU": {}
|
||||
},
|
||||
"f:labels": {
|
||||
"f:machine.openshift.io/cluster-api-machine-role": {},
|
||||
"f:machine.openshift.io/cluster-api-machine-type": {}
|
||||
}
|
||||
},
|
||||
"f:spec": {
|
||||
"f:template": {
|
||||
"f:metadata": {
|
||||
"f:labels": {
|
||||
"f:machine.openshift.io/cluster-api-machine-role": {},
|
||||
"f:machine.openshift.io/cluster-api-machine-type": {}
|
||||
}
|
||||
},
|
||||
"f:spec": {
|
||||
"f:metadata": {
|
||||
"f:labels": {
|
||||
"f:node-role.kubernetes.io/worker": {},
|
||||
"f:spot": {}
|
||||
}
|
||||
},
|
||||
"f:providerSpec": {
|
||||
"f:value": {
|
||||
"f:image": {},
|
||||
"f:location": {},
|
||||
"f:networkResourceGroup": {},
|
||||
"f:publicLoadBalancer": {},
|
||||
"f:resourceGroup": {},
|
||||
"f:subnet": {},
|
||||
"f:vmSize": {},
|
||||
"f:vnet": {}
|
||||
}
|
||||
},
|
||||
"f:taints": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"manager": "Mozilla",
|
||||
"operation": "Update",
|
||||
"time": "2021-08-19T19:22:35Z"
|
||||
},
|
||||
{
|
||||
"apiVersion": "machine.openshift.io/v1beta1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:spec": {
|
||||
"f:replicas": {}
|
||||
}
|
||||
},
|
||||
"manager": "kubectl-edit",
|
||||
"operation": "Update",
|
||||
"time": "2021-10-04T14:02:47Z"
|
||||
},
|
||||
{
|
||||
"apiVersion": "machine.openshift.io/v1beta1",
|
||||
"fieldsType": "FieldsV1",
|
||||
"fieldsV1": {
|
||||
"f:status": {
|
||||
"f:observedGeneration": {},
|
||||
"f:replicas": {}
|
||||
}
|
||||
},
|
||||
"manager": "machineset-controller",
|
||||
"operation": "Update",
|
||||
"time": "2021-10-04T14:02:48Z"
|
||||
}
|
||||
],
|
||||
"name": "aro-v4-shared-gxqb4-spot-eastus",
|
||||
"namespace": "openshift-machine-api",
|
||||
"resourceVersion": "213815832",
|
||||
"selfLink": "/apis/machine.openshift.io/v1beta1/namespaces/openshift-machine-api/machinesets/aro-v4-shared-gxqb4-spot-eastus",
|
||||
"uid": "3480c665-5090-48cd-9d39-7c29c26402ef"
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 0,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machineset": "aro-v4-shared-gxqb4-spot-eastus"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machine-role": "worker",
|
||||
"machine.openshift.io/cluster-api-machine-type": "worker",
|
||||
"machine.openshift.io/cluster-api-machineset": "aro-v4-shared-gxqb4-spot-eastus"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"node-role.kubernetes.io/worker": "",
|
||||
"spot": "true"
|
||||
}
|
||||
},
|
||||
"providerSpec": {
|
||||
"value": {
|
||||
"apiVersion": "azureproviderconfig.openshift.io/v1beta1",
|
||||
"credentialsSecret": {
|
||||
"name": "azure-cloud-credentials",
|
||||
"namespace": "openshift-machine-api"
|
||||
},
|
||||
"image": {
|
||||
"offer": "aro4",
|
||||
"publisher": "azureopenshift",
|
||||
"resourceID": "",
|
||||
"sku": "aro_43",
|
||||
"version": "43.81.20200311"
|
||||
},
|
||||
"kind": "AzureMachineProviderSpec",
|
||||
"location": "eastus",
|
||||
"metadata": {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"networkResourceGroup": "aro-v4-shared",
|
||||
"osDisk": {
|
||||
"diskSizeGB": 128,
|
||||
"managedDisk": {
|
||||
"storageAccountType": "Premium_LRS"
|
||||
},
|
||||
"osType": "Linux"
|
||||
},
|
||||
"publicIP": false,
|
||||
"publicLoadBalancer": "aro-v4-shared-gxqb4",
|
||||
"resourceGroup": "aro-v4-shared-cluster",
|
||||
"spotVMOptions": {},
|
||||
"subnet": "worker-subnet",
|
||||
"userDataSecret": {
|
||||
"name": "worker-user-data"
|
||||
},
|
||||
"vmSize": "Standard_D4s_v3",
|
||||
"vnet": "vnet",
|
||||
"zone": "1"
|
||||
}
|
||||
},
|
||||
"taints": [
|
||||
{
|
||||
"effect": "NoExecute",
|
||||
"key": "spot",
|
||||
"value": "true"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"observedGeneration": 25,
|
||||
"replicas": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "machine.openshift.io/v1beta1",
|
||||
"kind": "MachineSet",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"machine.openshift.io/v1beta1\",\"kind\":\"MachineSet\",\"metadata\":{\"annotations\":{\"machine.openshift.io/GPU\":\"0\",\"machine.openshift.io/memoryMb\":\"16384\",\"machine.openshift.io/vCPU\":\"4\"},\"creationTimestamp\":\"2020-05-05T22:42:46Z\",\"generation\":16,\"labels\":{\"machine.openshift.io/cluster-api-cluster\":\"aro-v4-shared-gxqb4\",\"machine.openshift.io/cluster-api-machine-role\":\"worker\",\"machine.openshift.io/cluster-api-machine-type\":\"worker\"},\"name\":\"aro-v4-shared-gxqb4-worker-eastus1\",\"namespace\":\"openshift-machine-api\",\"resourceVersion\":\"106123369\",\"selfLink\":\"/apis/machine.openshift.io/v1beta1/namespaces/openshift-machine-api/machinesets/aro-v4-shared-gxqb4-worker-eastus1\",\"uid\":\"3f77c6ec-975c-49e4-944d-0a58e30be390\"},\"spec\":{\"replicas\":1,\"selector\":{\"matchLabels\":{\"machine.openshift.io/cluster-api-cluster\":\"aro-v4-shared-gxqb4\",\"machine.openshift.io/cluster-api-machineset\":\"aro-v4-shared-gxqb4-worker-eastus1\"}},\"template\":{\"metadata\":{\"labels\":{\"machine.openshift.io/cluster-api-cluster\":\"aro-v4-shared-gxqb4\",\"machine.openshift.io/cluster-api-machine-role\":\"worker\",\"machine.openshift.io/cluster-api-machine-type\":\"worker\",\"machine.openshift.io/cluster-api-machineset\":\"aro-v4-shared-gxqb4-worker-eastus1\"}},\"spec\":{\"metadata\":{},\"providerSpec\":{\"value\":{\"apiVersion\":\"azureproviderconfig.openshift.io/v1beta1\",\"credentialsSecret\":{\"name\":\"azure-cloud-credentials\",\"namespace\":\"openshift-machine-api\"},\"image\":{\"offer\":\"aro4\",\"publisher\":\"azureopenshift\",\"resourceID\":\"\",\"sku\":\"aro_43\",\"version\":\"43.81.20200311\"},\"kind\":\"AzureMachineProviderSpec\",\"location\":\"eastus\",\"managedIdentity\":\"aro-v4-shared-gxqb4-identity\",\"metadata\":{\"creationTimestamp\":null},\"networkResourceGroup\":\"aro-v4-shared\",\"osDisk\":{\"diskSizeGB\":128,\"managedDisk\":{\"storageAccountType\":\"Premium_LRS\"},\"osType\":\"Linux\"},\"publicIP\":false,\"publicLoadBalancer\":\"aro-v4-shared-gxqb4\",\"resourceGroup\":\"aro-v4-shared-cluster\",\"subnet\":\"worker-subnet\",\"userDataSecret\":{\"name\":\"worker-user-data\"},\"vmSize\":\"Standard_D4s_v3\",\"vnet\":\"vnet\",\"zone\":\"1\"}}}}},\"status\":{\"availableReplicas\":1,\"fullyLabeledReplicas\":1,\"observedGeneration\":16,\"readyReplicas\":1,\"replicas\":1}}\n",
|
||||
"machine.openshift.io/GPU": "0",
|
||||
"machine.openshift.io/memoryMb": "16384",
|
||||
"machine.openshift.io/vCPU": "4"
|
||||
},
|
||||
"creationTimestamp": "2020-05-05T22:42:46Z",
|
||||
"generation": 20,
|
||||
"labels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machine-role": "worker",
|
||||
"machine.openshift.io/cluster-api-machine-type": "worker"
|
||||
},
|
||||
"name": "aro-v4-shared-gxqb4-worker-eastus1",
|
||||
"namespace": "openshift-machine-api",
|
||||
"resourceVersion": "218361971",
|
||||
"selfLink": "/apis/machine.openshift.io/v1beta1/namespaces/openshift-machine-api/machinesets/aro-v4-shared-gxqb4-worker-eastus1",
|
||||
"uid": "3f77c6ec-975c-49e4-944d-0a58e30be390"
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 1,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machineset": "aro-v4-shared-gxqb4-worker-eastus1"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machine-role": "worker",
|
||||
"machine.openshift.io/cluster-api-machine-type": "worker",
|
||||
"machine.openshift.io/cluster-api-machineset": "aro-v4-shared-gxqb4-worker-eastus1"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"metadata": {},
|
||||
"providerSpec": {
|
||||
"value": {
|
||||
"apiVersion": "azureproviderconfig.openshift.io/v1beta1",
|
||||
"credentialsSecret": {
|
||||
"name": "azure-cloud-credentials",
|
||||
"namespace": "openshift-machine-api"
|
||||
},
|
||||
"image": {
|
||||
"offer": "aro4",
|
||||
"publisher": "azureopenshift",
|
||||
"resourceID": "",
|
||||
"sku": "aro_43",
|
||||
"version": "43.81.20200311"
|
||||
},
|
||||
"kind": "AzureMachineProviderSpec",
|
||||
"location": "eastus",
|
||||
"metadata": {},
|
||||
"networkResourceGroup": "aro-v4-shared",
|
||||
"osDisk": {
|
||||
"diskSizeGB": 128,
|
||||
"managedDisk": {
|
||||
"storageAccountType": "Premium_LRS"
|
||||
},
|
||||
"osType": "Linux"
|
||||
},
|
||||
"publicIP": false,
|
||||
"publicLoadBalancer": "aro-v4-shared-gxqb4",
|
||||
"resourceGroup": "aro-v4-shared-cluster",
|
||||
"subnet": "worker-subnet",
|
||||
"userDataSecret": {
|
||||
"name": "worker-user-data"
|
||||
},
|
||||
"vmSize": "Standard_D4s_v3",
|
||||
"vnet": "vnet",
|
||||
"zone": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"availableReplicas": 1,
|
||||
"fullyLabeledReplicas": 1,
|
||||
"observedGeneration": 20,
|
||||
"readyReplicas": 1,
|
||||
"replicas": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "machine.openshift.io/v1beta1",
|
||||
"kind": "MachineSet",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"machine.openshift.io/GPU": "0",
|
||||
"machine.openshift.io/memoryMb": "16384",
|
||||
"machine.openshift.io/vCPU": "4"
|
||||
},
|
||||
"creationTimestamp": "2020-05-05T22:42:46Z",
|
||||
"generation": 3,
|
||||
"labels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machine-role": "worker",
|
||||
"machine.openshift.io/cluster-api-machine-type": "worker"
|
||||
},
|
||||
"name": "aro-v4-shared-gxqb4-worker-eastus2",
|
||||
"namespace": "openshift-machine-api",
|
||||
"resourceVersion": "218363792",
|
||||
"selfLink": "/apis/machine.openshift.io/v1beta1/namespaces/openshift-machine-api/machinesets/aro-v4-shared-gxqb4-worker-eastus2",
|
||||
"uid": "5a61ce63-6c82-45ca-b2ae-b84ca471de7e"
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 1,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machineset": "aro-v4-shared-gxqb4-worker-eastus2"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machine-role": "worker",
|
||||
"machine.openshift.io/cluster-api-machine-type": "worker",
|
||||
"machine.openshift.io/cluster-api-machineset": "aro-v4-shared-gxqb4-worker-eastus2"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"metadata": {},
|
||||
"providerSpec": {
|
||||
"value": {
|
||||
"apiVersion": "azureproviderconfig.openshift.io/v1beta1",
|
||||
"credentialsSecret": {
|
||||
"name": "azure-cloud-credentials",
|
||||
"namespace": "openshift-machine-api"
|
||||
},
|
||||
"image": {
|
||||
"offer": "aro4",
|
||||
"publisher": "azureopenshift",
|
||||
"resourceID": "",
|
||||
"sku": "aro_43",
|
||||
"version": "43.81.20200311"
|
||||
},
|
||||
"kind": "AzureMachineProviderSpec",
|
||||
"location": "eastus",
|
||||
"metadata": {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"networkResourceGroup": "aro-v4-shared",
|
||||
"osDisk": {
|
||||
"diskSizeGB": 128,
|
||||
"managedDisk": {
|
||||
"storageAccountType": "Premium_LRS"
|
||||
},
|
||||
"osType": "Linux"
|
||||
},
|
||||
"publicIP": false,
|
||||
"publicLoadBalancer": "aro-v4-shared-gxqb4",
|
||||
"resourceGroup": "aro-v4-shared-cluster",
|
||||
"subnet": "worker-subnet",
|
||||
"userDataSecret": {
|
||||
"name": "worker-user-data"
|
||||
},
|
||||
"vmSize": "Standard_D4s_v3",
|
||||
"vnet": "vnet",
|
||||
"zone": "2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"availableReplicas": 1,
|
||||
"fullyLabeledReplicas": 1,
|
||||
"observedGeneration": 3,
|
||||
"readyReplicas": 1,
|
||||
"replicas": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"apiVersion": "machine.openshift.io/v1beta1",
|
||||
"kind": "MachineSet",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"machine.openshift.io/GPU": "0",
|
||||
"machine.openshift.io/memoryMb": "16384",
|
||||
"machine.openshift.io/vCPU": "4"
|
||||
},
|
||||
"creationTimestamp": "2020-05-05T22:42:46Z",
|
||||
"generation": 7,
|
||||
"labels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machine-role": "worker",
|
||||
"machine.openshift.io/cluster-api-machine-type": "worker"
|
||||
},
|
||||
"name": "aro-v4-shared-gxqb4-worker-eastus3",
|
||||
"namespace": "openshift-machine-api",
|
||||
"resourceVersion": "218360062",
|
||||
"selfLink": "/apis/machine.openshift.io/v1beta1/namespaces/openshift-machine-api/machinesets/aro-v4-shared-gxqb4-worker-eastus3",
|
||||
"uid": "194e3936-21d9-4f81-97d1-400f1767d2a8"
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 1,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machineset": "aro-v4-shared-gxqb4-worker-eastus3"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"machine.openshift.io/cluster-api-cluster": "aro-v4-shared-gxqb4",
|
||||
"machine.openshift.io/cluster-api-machine-role": "worker",
|
||||
"machine.openshift.io/cluster-api-machine-type": "worker",
|
||||
"machine.openshift.io/cluster-api-machineset": "aro-v4-shared-gxqb4-worker-eastus3"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"metadata": {},
|
||||
"providerSpec": {
|
||||
"value": {
|
||||
"apiVersion": "azureproviderconfig.openshift.io/v1beta1",
|
||||
"credentialsSecret": {
|
||||
"name": "azure-cloud-credentials",
|
||||
"namespace": "openshift-machine-api"
|
||||
},
|
||||
"image": {
|
||||
"offer": "aro4",
|
||||
"publisher": "azureopenshift",
|
||||
"resourceID": "",
|
||||
"sku": "aro_43",
|
||||
"version": "43.81.20200311"
|
||||
},
|
||||
"kind": "AzureMachineProviderSpec",
|
||||
"location": "eastus",
|
||||
"metadata": {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"networkResourceGroup": "aro-v4-shared",
|
||||
"osDisk": {
|
||||
"diskSizeGB": 128,
|
||||
"managedDisk": {
|
||||
"storageAccountType": "Premium_LRS"
|
||||
},
|
||||
"osType": "Linux"
|
||||
},
|
||||
"publicIP": false,
|
||||
"publicLoadBalancer": "aro-v4-shared-gxqb4",
|
||||
"resourceGroup": "aro-v4-shared-cluster",
|
||||
"subnet": "worker-subnet",
|
||||
"userDataSecret": {
|
||||
"name": "worker-user-data"
|
||||
},
|
||||
"vmSize": "Standard_D4s_v3",
|
||||
"vnet": "vnet",
|
||||
"zone": "3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"availableReplicas": 1,
|
||||
"fullyLabeledReplicas": 1,
|
||||
"observedGeneration": 7,
|
||||
"readyReplicas": 1,
|
||||
"replicas": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"kind": "MachineSetList",
|
||||
"metadata": {
|
||||
"resourceVersion": "",
|
||||
"selfLink": ""
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -40,7 +40,7 @@ func (p *portal) clusterInfo(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
subscription := apiVars["subscription"]
|
||||
resourceGroup := apiVars["resourceGroup"]
|
||||
clusterName := apiVars["name"]
|
||||
clusterName := apiVars["clusterName"]
|
||||
|
||||
resourceId := strings.ToLower(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.RedHatOpenShift/openShiftClusters/%s", subscription, resourceGroup, clusterName))
|
||||
|
||||
|
|
|
@ -98,6 +98,7 @@ func TestClusterList(t *testing.T) {
|
|||
ResourceGroup: "resourceGroupName",
|
||||
Subscription: "00000000-0000-0000-0000-000000000000",
|
||||
CreatedAt: "2011-01-02T01:03:00Z",
|
||||
LastModified: "Unknown",
|
||||
ProvisioningState: api.ProvisioningStateSucceeded.String(),
|
||||
},
|
||||
{
|
||||
|
@ -107,6 +108,7 @@ func TestClusterList(t *testing.T) {
|
|||
ResourceGroup: "resourceGroupName",
|
||||
Subscription: "00000000-0000-0000-0000-000000000000",
|
||||
CreatedAt: "Unknown",
|
||||
LastModified: "Unknown",
|
||||
ProvisioningState: api.ProvisioningStateCreating.String(),
|
||||
},
|
||||
|
||||
|
@ -117,6 +119,7 @@ func TestClusterList(t *testing.T) {
|
|||
ResourceGroup: "resourceGroupName",
|
||||
Subscription: "00000000-0000-0000-0000-000000000000",
|
||||
CreatedAt: "Unknown",
|
||||
LastModified: "Unknown",
|
||||
ProvisioningState: api.ProvisioningStateFailed.String(),
|
||||
FailedProvisioningState: api.ProvisioningStateCreating.String(),
|
||||
},
|
||||
|
@ -162,6 +165,9 @@ func TestClusterDetail(t *testing.T) {
|
|||
URL: "example.com",
|
||||
},
|
||||
},
|
||||
SystemData: api.SystemData{
|
||||
LastModifiedAt: &parsedTime,
|
||||
},
|
||||
},
|
||||
},
|
||||
&api.OpenShiftClusterDocument{
|
||||
|
|
|
@ -282,8 +282,12 @@ func (p *portal) aadAuthenticatedRoutes(r *mux.Router) {
|
|||
r.NewRoute().Methods(http.MethodGet).Path("/api/info").HandlerFunc(p.info)
|
||||
|
||||
// Cluster-specific routes
|
||||
r.NewRoute().PathPrefix("/api/{subscription}/{resourceGroup}/{name}/clusteroperators").HandlerFunc(p.clusterOperators)
|
||||
r.NewRoute().Methods(http.MethodGet).Path("/api/{subscription}/{resourceGroup}/{name}").HandlerFunc(p.clusterInfo)
|
||||
r.NewRoute().PathPrefix("/api/{subscription}/{resourceGroup}/{clusterName}/clusteroperators").HandlerFunc(p.clusterOperators)
|
||||
r.NewRoute().Methods(http.MethodGet).Path("/api/{subscription}/{resourceGroup}/{clusterName}").HandlerFunc(p.clusterInfo)
|
||||
r.NewRoute().PathPrefix("/api/{subscription}/{resourceGroup}/{clusterName}/nodes").HandlerFunc(p.nodes)
|
||||
r.NewRoute().PathPrefix("/api/{subscription}/{resourceGroup}/{clusterName}/machines").HandlerFunc(p.machines)
|
||||
r.NewRoute().PathPrefix("/api/{subscription}/{resourceGroup}/{clusterName}/machine-sets").HandlerFunc(p.machineSets)
|
||||
r.NewRoute().PathPrefix("/api/{subscription}/{resourceGroup}/{clusterName}").HandlerFunc(p.clusterInfo)
|
||||
}
|
||||
|
||||
func (p *portal) index(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -318,7 +322,14 @@ func (p *portal) indexV2(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// makeFetcher creates a cluster.FetchClient suitable for use by the Portal REST API
|
||||
func (p *portal) makeFetcher(ctx context.Context, r *http.Request) (cluster.FetchClient, error) {
|
||||
resourceID := strings.Join(strings.Split(r.URL.Path, "/")[:9], "/")
|
||||
apiVars := mux.Vars(r)
|
||||
|
||||
subscription := apiVars["subscription"]
|
||||
resourceGroup := apiVars["resourceGroup"]
|
||||
clusterName := apiVars["clusterName"]
|
||||
|
||||
resourceID := strings.ToLower(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.RedHatOpenShift/openShiftClusters/%s", subscription, resourceGroup, clusterName))
|
||||
|
||||
if !validate.RxClusterID.MatchString(resourceID) {
|
||||
return nil, fmt.Errorf("invalid resource ID")
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"files": {
|
||||
"main.js": "/static/js/main.2de0ba6f.js",
|
||||
"main.js": "/static/js/main.bb644f09.js",
|
||||
"index.html": "/index.html",
|
||||
"main.2de0ba6f.js.map": "/static/js/main.2de0ba6f.js.map"
|
||||
"main.bb644f09.js.map": "/static/js/main.bb644f09.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/js/main.2de0ba6f.js"
|
||||
"static/js/main.bb644f09.js"
|
||||
]
|
||||
}
|
|
@ -1 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><title>ARO Portal</title><script defer="defer" src="/static/js/main.2de0ba6f.js"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><title>ARO Portal</title><script defer="defer" src="/static/js/main.bb644f09.js"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -5,7 +5,7 @@
|
|||
"license": "Apache2",
|
||||
"proxy": "https://localhost:8444",
|
||||
"dependencies": {
|
||||
"@fluentui/react": "^8.49.6",
|
||||
"@fluentui/react": "^8.97.2",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/node": "^17.0.8",
|
||||
|
|
|
@ -267,7 +267,7 @@ function App() {
|
|||
<Stack.Item grow>
|
||||
<ClusterDetailPanel
|
||||
csrfToken={csrfRef}
|
||||
csrfTokenAvailable={fetching}
|
||||
loaded={fetching}
|
||||
currentCluster={currentCluster}
|
||||
onClose={_onCloseDetailPanel}
|
||||
/>
|
||||
|
|
|
@ -13,12 +13,13 @@ import {
|
|||
Separator,
|
||||
IStackStyles,
|
||||
Icon,
|
||||
IconButton,
|
||||
IIconStyles,
|
||||
} from "@fluentui/react"
|
||||
import { AxiosResponse } from "axios"
|
||||
import { FetchClusterInfo } from "./Request"
|
||||
import { ICluster, headerStyles } from "./App"
|
||||
import { Nav, INavLink, INavLinkGroup, INavStyles } from "@fluentui/react/lib/Nav"
|
||||
import { Nav, INavLink, INavStyles } from "@fluentui/react/lib/Nav"
|
||||
import { ClusterDetailComponent } from "./ClusterDetailList"
|
||||
import React from "react"
|
||||
|
||||
|
@ -36,36 +37,14 @@ const navStyles: Partial<INavStyles> = {
|
|||
},
|
||||
}
|
||||
|
||||
const navLinkGroups: INavLinkGroup[] = [
|
||||
{
|
||||
links: [
|
||||
{
|
||||
name: "Overview",
|
||||
key: "overview",
|
||||
url: "#overview",
|
||||
icon: "ThisPC",
|
||||
},
|
||||
],
|
||||
},
|
||||
/* {
|
||||
links: [
|
||||
{
|
||||
name: "Nodes",
|
||||
key: "nodes",
|
||||
url: "#nodes",
|
||||
icon: "BuildQueue",
|
||||
},
|
||||
],
|
||||
}, */
|
||||
]
|
||||
|
||||
const customPanelStyle: Partial<IPanelStyles> = {
|
||||
root: { top: "40px", left: "225px" },
|
||||
content: { paddingLeft: 30, paddingRight: 5 },
|
||||
navigation: {
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
}
|
||||
// let customPanelStyle: Partial<IPanelStyles> = {
|
||||
// root: { top: "40px", left: "225px" },
|
||||
// content: { paddingLeft: 30, paddingRight: 5 },
|
||||
// navigation: {
|
||||
// justifyContent: "flex-start",
|
||||
// },
|
||||
// }
|
||||
|
||||
const headerStyle: Partial<IStackStyles> = {
|
||||
root: {
|
||||
|
@ -77,6 +56,15 @@ const headerStyle: Partial<IStackStyles> = {
|
|||
},
|
||||
}
|
||||
|
||||
const doubleChevronIconStyle: Partial<IStackStyles> = {
|
||||
root: {
|
||||
marginLeft: -30,
|
||||
marginTop: -15,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
},
|
||||
}
|
||||
|
||||
const headerIconStyles: Partial<IIconStyles> = {
|
||||
root: {
|
||||
height: "100%",
|
||||
|
@ -89,13 +77,18 @@ const headerIconStyles: Partial<IIconStyles> = {
|
|||
},
|
||||
}
|
||||
|
||||
export const overviewKey = "overview"
|
||||
export const nodesKey = "nodes"
|
||||
export const machinesKey = "machines"
|
||||
export const machineSetsKey = "machinesets"
|
||||
|
||||
const errorBarStyles: Partial<IMessageBarStyles> = { root: { marginBottom: 15 } }
|
||||
|
||||
export function ClusterDetailPanel(props: {
|
||||
csrfToken: MutableRefObject<string>
|
||||
currentCluster: ICluster | null
|
||||
onClose: Function
|
||||
csrfTokenAvailable: string
|
||||
onClose: any
|
||||
loaded: string
|
||||
}) {
|
||||
const [data, setData] = useState<any>([])
|
||||
const [error, setError] = useState<AxiosResponse | null>(null)
|
||||
|
@ -104,6 +97,13 @@ export function ClusterDetailPanel(props: {
|
|||
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false) // panel controls
|
||||
const [dataLoaded, setDataLoaded] = useState<boolean>(false)
|
||||
const [detailPanelVisible, setdetailPanelVisible] = useState<string>("Overview")
|
||||
const [customPanelStyle, setcustomPanelStyle] = useState<Partial<IPanelStyles>>({
|
||||
root: { top: "40px", left: "225px" },
|
||||
content: { paddingLeft: 30, paddingRight: 5 },
|
||||
navigation: {
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
})
|
||||
|
||||
const errorBar = (): any => {
|
||||
return (
|
||||
|
@ -118,6 +118,37 @@ export function ClusterDetailPanel(props: {
|
|||
)
|
||||
}
|
||||
|
||||
const navLinkGroups = [
|
||||
{
|
||||
links: [
|
||||
{
|
||||
name: 'Overview',
|
||||
key: overviewKey,
|
||||
url: '#overview',
|
||||
icon: 'ThisPC',
|
||||
},
|
||||
{
|
||||
name: 'Nodes',
|
||||
key: nodesKey,
|
||||
url: '#nodes',
|
||||
icon: 'BuildQueue',
|
||||
},
|
||||
{
|
||||
name: 'Machines',
|
||||
key: machinesKey,
|
||||
url: '#machines',
|
||||
icon: 'BuildQueue',
|
||||
},
|
||||
{
|
||||
name: 'MachineSets',
|
||||
key: machineSetsKey,
|
||||
url: '#machinesets',
|
||||
icon: 'BuildQueue',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// updateData - updates the state of the component
|
||||
// can be used if we want a refresh button.
|
||||
// api/clusterdetail returns a single item.
|
||||
|
@ -141,7 +172,7 @@ export function ClusterDetailPanel(props: {
|
|||
if (props.currentCluster == null) {
|
||||
return
|
||||
}
|
||||
var resourceID = props.currentCluster.resourceId
|
||||
const resourceID = props.currentCluster.resourceId
|
||||
|
||||
const onData = (result: AxiosResponse | null) => {
|
||||
if (result?.status === 200) {
|
||||
|
@ -153,10 +184,10 @@ export function ClusterDetailPanel(props: {
|
|||
setFetching(resourceID)
|
||||
}
|
||||
|
||||
if (fetching === "" && props.csrfTokenAvailable === "DONE" && resourceID != "") {
|
||||
if (fetching === "" && props.loaded === "DONE" && resourceID != "") {
|
||||
setFetching("FETCHING")
|
||||
setError(null)
|
||||
FetchClusterInfo(props.currentCluster).then(onData) // TODO: fetchClusterInfo accepts IClusterDetail
|
||||
FetchClusterInfo(props.currentCluster).then(onData)
|
||||
}
|
||||
}, [data, fetching, setFetching])
|
||||
|
||||
|
@ -165,7 +196,7 @@ export function ClusterDetailPanel(props: {
|
|||
setDataLoaded(false)
|
||||
return
|
||||
}
|
||||
var resourceID = props.currentCluster.resourceId
|
||||
const resourceID = props.currentCluster.resourceId
|
||||
|
||||
if (resourceID != "") {
|
||||
if (resourceID == fetching) {
|
||||
|
@ -186,10 +217,40 @@ export function ClusterDetailPanel(props: {
|
|||
}
|
||||
}
|
||||
|
||||
const [doubleChevronIconProp, setdoubleChevronIconProp] = useState({ iconName: "doublechevronleft"})
|
||||
function _onClickDoubleChevronIcon() {
|
||||
let customPanelStyleRootLeft
|
||||
if (doubleChevronIconProp.iconName == "doublechevronright") {
|
||||
customPanelStyleRootLeft = "225px"
|
||||
setdoubleChevronIconProp({ iconName: "doublechevronleft"})
|
||||
} else {
|
||||
customPanelStyleRootLeft = "0px"
|
||||
setdoubleChevronIconProp({ iconName: "doublechevronright"})
|
||||
}
|
||||
|
||||
setcustomPanelStyle({
|
||||
root: { top: "40px", left: customPanelStyleRootLeft },
|
||||
content: { paddingLeft: 30, paddingRight: 5 },
|
||||
navigation: {
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const onRenderHeader = (
|
||||
): ReactElement => {
|
||||
return (
|
||||
<>
|
||||
<Stack styles={headerStyle} horizontal>
|
||||
<Stack.Item styles={doubleChevronIconStyle}>
|
||||
<IconButton
|
||||
onClick={_onClickDoubleChevronIcon}
|
||||
iconProps={doubleChevronIconProp}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
|
||||
<Stack styles={headerStyle} horizontal>
|
||||
<Stack.Item>
|
||||
<Icon styles={headerIconStyles} iconName="openshift-svg"></Icon>
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { IShimmerStyles, Shimmer, ShimmerElementType } from "@fluentui/react/lib/Shimmer"
|
||||
import { Component } from "react"
|
||||
import { Stack, Text, IStackStyles, IStackItemStyles } from "@fluentui/react"
|
||||
import { contentStackStylesNormal, ICluster } from "./App"
|
||||
import { OverviewWrapper } from './ClusterDetailListComponents/OverviewWrapper';
|
||||
import { NodesWrapper } from './ClusterDetailListComponents/NodesWrapper';
|
||||
import { MachinesWrapper } from "./ClusterDetailListComponents/MachinesWrapper";
|
||||
import { MachineSetsWrapper } from "./ClusterDetailListComponents/MachineSetsWrapper";
|
||||
import { ICluster } from "./App"
|
||||
|
||||
interface ClusterDetailComponentProps {
|
||||
item: any
|
||||
|
@ -10,10 +12,6 @@ interface ClusterDetailComponentProps {
|
|||
detailPanelVisible: string
|
||||
}
|
||||
|
||||
interface IClusterDetailComponentState {
|
||||
item: IClusterDetails // why both state and props?
|
||||
}
|
||||
|
||||
export interface IClusterDetails {
|
||||
apiServerVisibility: string
|
||||
apiServerURL: string
|
||||
|
@ -29,169 +27,49 @@ export interface IClusterDetails {
|
|||
lastProvisioningState: string
|
||||
location: string
|
||||
name: string
|
||||
resourceId: string
|
||||
provisioningState: string
|
||||
version: string
|
||||
installStatus: string
|
||||
}
|
||||
|
||||
const clusterDetailHeadings: IClusterDetails = {
|
||||
apiServerVisibility: "ApiServer Visibility",
|
||||
apiServerURL: "ApiServer URL",
|
||||
architectureVersion: "Architecture Version",
|
||||
consoleLink: "Console Link",
|
||||
createdAt: "Created At",
|
||||
createdBy: "Created By",
|
||||
failedProvisioningState: "Failed Provisioning State",
|
||||
infraId: "Infra Id",
|
||||
lastAdminUpdateError: "Last Admin Update Error",
|
||||
lastModifiedAt: "Last Modified At",
|
||||
lastModifiedBy: "Last Modified By",
|
||||
lastProvisioningState: "Last Provisioning State",
|
||||
location: "Location",
|
||||
name: "Name",
|
||||
provisioningState: "Provisioning State",
|
||||
version: "Version",
|
||||
installStatus: "Installation Status",
|
||||
interface IClusterDetailComponentState {
|
||||
item: IClusterDetails // why both state and props?
|
||||
detailPanelSelected: string
|
||||
}
|
||||
|
||||
const ShimmerStyle: Partial<IShimmerStyles> = {
|
||||
root: {
|
||||
margin: "11px 0",
|
||||
},
|
||||
}
|
||||
export class ClusterDetailComponent extends Component<ClusterDetailComponentProps, IClusterDetailComponentState> {
|
||||
|
||||
const headShimmerStyle: Partial<IShimmerStyles> = {
|
||||
root: {
|
||||
margin: "15px 0",
|
||||
},
|
||||
}
|
||||
|
||||
const headerShimmer = [{ type: ShimmerElementType.line, height: 32, width: "25%" }]
|
||||
|
||||
const rowShimmer = [{ type: ShimmerElementType.line, height: 18, width: "75%" }]
|
||||
|
||||
const KeyColumnStyle: Partial<IStackStyles> = {
|
||||
root: {
|
||||
paddingTop: 10,
|
||||
paddingRight: 15,
|
||||
},
|
||||
}
|
||||
|
||||
const ValueColumnStyle: Partial<IStackStyles> = {
|
||||
root: {
|
||||
paddingTop: 10,
|
||||
},
|
||||
}
|
||||
|
||||
const KeyStyle: IStackItemStyles = {
|
||||
root: {
|
||||
fontStyle: "bold",
|
||||
alignSelf: "flex-start",
|
||||
fontVariantAlternates: "bold",
|
||||
color: "grey",
|
||||
paddingBottom: 10,
|
||||
},
|
||||
}
|
||||
|
||||
const ValueStyle: IStackItemStyles = {
|
||||
root: {
|
||||
paddingBottom: 10,
|
||||
},
|
||||
}
|
||||
|
||||
function ClusterDetailCell(value: any): any {
|
||||
if (typeof value.value == typeof " ") {
|
||||
return (
|
||||
<Stack.Item id="ClusterDetailCell" styles={value.style}>
|
||||
<Text styles={value.style} variant={"medium"}>
|
||||
{value.value}
|
||||
</Text>
|
||||
</Stack.Item>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export class ClusterDetailComponent extends Component<
|
||||
ClusterDetailComponentProps,
|
||||
IClusterDetailComponentState
|
||||
> {
|
||||
constructor(props: ClusterDetailComponentProps | Readonly<ClusterDetailComponentProps>) {
|
||||
super(props)
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const headerEntries = Object.entries(clusterDetailHeadings)
|
||||
switch (this.props.detailPanelVisible) {
|
||||
case "Overview":
|
||||
{
|
||||
if (this.props.item.length != 0) {
|
||||
return (
|
||||
<Stack styles={contentStackStylesNormal}>
|
||||
<Stack horizontal>
|
||||
<Stack id="Headers" styles={KeyColumnStyle}>
|
||||
{headerEntries.map((value: any, index: number) => (
|
||||
<ClusterDetailCell style={KeyStyle} key={index} value={value[1]} />
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Stack id="Colons" styles={KeyColumnStyle}>
|
||||
{Array(headerEntries.length)
|
||||
.fill(":")
|
||||
.map((value: any, index: number) => (
|
||||
<ClusterDetailCell style={KeyStyle} key={index} value={value} />
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Stack id="Values"styles={ValueColumnStyle}>
|
||||
{headerEntries.map((value: [any, any], index: number) => (
|
||||
<ClusterDetailCell
|
||||
style={ValueStyle}
|
||||
key={index}
|
||||
value={
|
||||
this.props.item[value[0]] != null &&
|
||||
this.props.item[value[0]].toString().length > 0
|
||||
? this.props.item[value[0]]
|
||||
: "Undefined"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Stack>
|
||||
<Shimmer
|
||||
styles={headShimmerStyle}
|
||||
shimmerElements={headerShimmer}
|
||||
width="25%"></Shimmer>
|
||||
{headerEntries.map((value: [any, any], index: number) => (
|
||||
<Shimmer
|
||||
styles={ShimmerStyle}
|
||||
key={index}
|
||||
shimmerElements={rowShimmer}
|
||||
width="75%"></Shimmer>
|
||||
))}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
}
|
||||
case "Nodes":
|
||||
switch (this.props.detailPanelVisible.toLowerCase()) {
|
||||
case "overview":
|
||||
{
|
||||
return (
|
||||
<OverviewWrapper clusterName= {this.props.item.name} currentCluster={this.props.cluster!} detailPanelSelected={this.props.detailPanelVisible} loaded={this.props.isDataLoaded}/>
|
||||
)
|
||||
}
|
||||
case "nodes":
|
||||
{
|
||||
return (
|
||||
<Stack styles={contentStackStylesNormal}>
|
||||
<Text variant="xxLarge">{this.props.cluster?.name}</Text>
|
||||
<Stack horizontal>
|
||||
<Stack styles={KeyColumnStyle}>Node detail</Stack>
|
||||
|
||||
<Stack styles={KeyColumnStyle}>Node detail2</Stack>
|
||||
|
||||
<Stack styles={ValueColumnStyle}>Node detail3</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
<NodesWrapper currentCluster={this.props.cluster!} detailPanelSelected={this.props.detailPanelVisible} loaded={this.props.isDataLoaded}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
case "machines":
|
||||
{
|
||||
return (
|
||||
<MachinesWrapper currentCluster={this.props.cluster!} detailPanelSelected={this.props.detailPanelVisible} loaded={this.props.isDataLoaded}/>
|
||||
);
|
||||
}
|
||||
case "machinesets":
|
||||
{
|
||||
return (
|
||||
<MachineSetsWrapper currentCluster={this.props.cluster!} detailPanelSelected={this.props.detailPanelVisible} loaded={this.props.isDataLoaded}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
import { IShimmerStyles, IStackItemStyles, IStackStyles, ShimmerElementType, Stack, Text } from "@fluentui/react"
|
||||
import { contentStackStylesNormal } from "../App"
|
||||
|
||||
export const ShimmerStyle: Partial<IShimmerStyles> = {
|
||||
root: {
|
||||
margin: "11px 0"
|
||||
}
|
||||
}
|
||||
|
||||
export const headShimmerStyle: Partial<IShimmerStyles> = {
|
||||
root: {
|
||||
margin: "15px 0"
|
||||
}
|
||||
}
|
||||
|
||||
export const headerShimmer = [
|
||||
{ type: ShimmerElementType.line, height: 32, width: '25%' },
|
||||
]
|
||||
|
||||
export const rowShimmer = [
|
||||
{ type: ShimmerElementType.line, height: 18, width: '75%' },
|
||||
]
|
||||
|
||||
export const KeyColumnStyle: Partial<IStackStyles> = {
|
||||
root: {
|
||||
paddingTop: 10,
|
||||
paddingRight: 15,
|
||||
}
|
||||
}
|
||||
|
||||
export const ValueColumnStyle: Partial<IStackStyles> = {
|
||||
root: {
|
||||
paddingTop: 10,
|
||||
}
|
||||
}
|
||||
|
||||
export const KeyStyle: IStackItemStyles = {
|
||||
root: {
|
||||
fontStyle: "bold",
|
||||
alignSelf: "flex-start",
|
||||
fontVariantAlternates: "bold",
|
||||
color: "grey",
|
||||
paddingBottom: 10
|
||||
}
|
||||
}
|
||||
|
||||
export const ValueStyle: IStackItemStyles = {
|
||||
root: {
|
||||
paddingBottom: 10
|
||||
}
|
||||
}
|
||||
|
||||
function Column(
|
||||
value: any,
|
||||
): any {
|
||||
if (typeof (value.value) == typeof (" ")) {
|
||||
return <Stack.Item styles={value.style}>
|
||||
<Text styles={value.style} variant={'medium'}>{value.value}</Text>
|
||||
</Stack.Item>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const InfoList = (
|
||||
props: {headers: any, object: any, title: string, titleSize: any}
|
||||
) => {
|
||||
const headerEntries = Object.entries(props.headers)
|
||||
const filteredHeaders: Array<[string, any]> = []
|
||||
headerEntries.filter((element: [string, any]) => {
|
||||
if (props.object[element[0]] != null &&
|
||||
props.object[element[0]].toString().length > 0) {
|
||||
filteredHeaders.push(element)
|
||||
}
|
||||
})
|
||||
return (
|
||||
<Stack styles={contentStackStylesNormal}>
|
||||
<Text variant={props.titleSize}>{props.title}</Text>
|
||||
<Stack horizontal>
|
||||
<Stack styles={KeyColumnStyle}>
|
||||
{filteredHeaders.map((value: [string, any], index: number) => (
|
||||
<Column style={KeyStyle} key={index} value={value[1]} />
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack styles={KeyColumnStyle}>
|
||||
{Array(filteredHeaders.length).fill(':').map((value: [string], index: number) => (
|
||||
<Column style={KeyStyle} key={index} value={value} />
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack styles={ValueColumnStyle}>
|
||||
{filteredHeaders.map((value: [string, any], index: number) => (
|
||||
<Column style={ValueStyle}
|
||||
key={index}
|
||||
value={props.object[value[0]]} />
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export const MultiInfoList = (
|
||||
props: any
|
||||
) => {
|
||||
return props.items.map((item: { [key: string]: any; }) => {
|
||||
return <InfoList key={item.key} headers={props.headers} object={item} title={item[props.subProp]} titleSize={item[props.titleSize]}/>
|
||||
})
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { Component } from "react"
|
||||
import { Stack, StackItem, PivotItem, IStackItemStyles, } from '@fluentui/react';
|
||||
import { contentStackStylesNormal } from "../App";
|
||||
import { InfoList } from "./InfoList"
|
||||
import { IMachineSet } from "./MachineSetsWrapper";
|
||||
|
||||
interface MachineSetsComponentProps {
|
||||
machineSets: any
|
||||
clusterName: string
|
||||
machineSetName: string
|
||||
}
|
||||
|
||||
const stackItemStyles: IStackItemStyles = {
|
||||
root: {
|
||||
width: "45%",
|
||||
},
|
||||
};
|
||||
|
||||
const MachineSetDetails: IMachineSet = {
|
||||
name: 'Name',
|
||||
type: "Type",
|
||||
createdAt: "Created Time",
|
||||
desiredReplicas: "Desired Replicas Count",
|
||||
replicas: "Actual Replicas Count",
|
||||
errorReason: "Error Reason",
|
||||
errorMessage: "Error Message",
|
||||
|
||||
}
|
||||
|
||||
interface IMachineSetsState {
|
||||
machineSets: IMachineSet[]
|
||||
}
|
||||
|
||||
const renderMachineSets = (machineSet: IMachineSet) => {
|
||||
return <PivotItem key={machineSet.name} headerText={machineSet.name}>
|
||||
<Stack styles={stackItemStyles}>
|
||||
<StackItem>
|
||||
<InfoList headers={MachineSetDetails} object={machineSet} title={machineSet.name!} titleSize="large"/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</PivotItem>;
|
||||
};
|
||||
|
||||
export class MachineSetsComponent extends Component<MachineSetsComponentProps, IMachineSetsState> {
|
||||
|
||||
constructor(props: MachineSetsComponentProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
machineSets: this.props.machineSets,
|
||||
}
|
||||
}
|
||||
|
||||
private extractCurrentMachineSet = (machineSetName: string): IMachineSet => {
|
||||
let machineSetTemp: IMachineSet
|
||||
this.state.machineSets.forEach((machineSet: IMachineSet) => {
|
||||
if (machineSet.name === machineSetName) {
|
||||
machineSetTemp = machineSet
|
||||
return
|
||||
}
|
||||
})
|
||||
return machineSetTemp!
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Stack styles={contentStackStylesNormal}>
|
||||
<Stack>
|
||||
{renderMachineSets(this.extractCurrentMachineSet(this.props.machineSetName))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,226 @@
|
|||
import * as React from 'react';
|
||||
import { useState, useEffect } from "react"
|
||||
import { Stack, StackItem, IconButton, IIconStyles, SelectionMode } from '@fluentui/react';
|
||||
import { Link } from '@fluentui/react/lib/Link';
|
||||
import { IColumn } from '@fluentui/react/lib/DetailsList';
|
||||
import { ShimmeredDetailsList } from '@fluentui/react/lib/ShimmeredDetailsList';
|
||||
import { IMachineSet } from "./MachineSetsWrapper";
|
||||
import { MachineSetsComponent } from "./MachineSets"
|
||||
|
||||
|
||||
|
||||
export declare interface IMachineSetsList {
|
||||
name?: string;
|
||||
desiredReplicas: string;
|
||||
currentReplicas: string;
|
||||
publicLoadBalancer?: string;
|
||||
storageType?: string
|
||||
}
|
||||
|
||||
interface MachineSetsListComponentProps {
|
||||
machineSets: any
|
||||
clusterName: string
|
||||
}
|
||||
|
||||
export interface IMachineSetsListState {
|
||||
machineSets: IMachineSet[]
|
||||
clusterName: string
|
||||
}
|
||||
|
||||
export class MachineSetsListComponent extends React.Component<MachineSetsListComponentProps, IMachineSetsListState> {
|
||||
|
||||
constructor(props: MachineSetsListComponentProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
machineSets: this.props.machineSets,
|
||||
clusterName: this.props.clusterName,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<MachineSetsListHelperComponent machineSets={this.state.machineSets} clusterName={this.state.clusterName}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function MachineSetsListHelperComponent(props: {
|
||||
machineSets: any,
|
||||
clusterName: string
|
||||
}) {
|
||||
const [columns, setColumns] = useState<IColumn[]>([
|
||||
{
|
||||
key: "machineName",
|
||||
name: "Name",
|
||||
fieldName: "name",
|
||||
minWidth: 80,
|
||||
maxWidth: 300,
|
||||
isResizable: true,
|
||||
isSorted: true,
|
||||
isSortedDescending: false,
|
||||
showSortIconWhenUnsorted: true,
|
||||
onRender: (item: IMachineSetsList) => (
|
||||
<Link onClick={() => _onMachineInfoLinkClick(item.name!)}>{item.name}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "desiredReplicas",
|
||||
name: "Desired Replicas",
|
||||
fieldName: "desiredReplicas",
|
||||
minWidth: 80,
|
||||
maxWidth: 120,
|
||||
isResizable: true,
|
||||
isSorted: true,
|
||||
isSortedDescending: false,
|
||||
showSortIconWhenUnsorted: true,
|
||||
},
|
||||
{
|
||||
key: "currentReplicas",
|
||||
name: "Current Replicas",
|
||||
fieldName: "currentReplicas",
|
||||
minWidth: 80,
|
||||
maxWidth: 120,
|
||||
isResizable: true,
|
||||
isSorted: true,
|
||||
isSortedDescending: false,
|
||||
showSortIconWhenUnsorted: true,
|
||||
},
|
||||
{
|
||||
key: "publicLoadBalancer",
|
||||
name: "Public LoadBalancer",
|
||||
fieldName: "publicLoadBalancer",
|
||||
minWidth: 150,
|
||||
maxWidth: 150,
|
||||
isResizable: true,
|
||||
isSorted: true,
|
||||
isSortedDescending: false,
|
||||
showSortIconWhenUnsorted: true,
|
||||
},
|
||||
{
|
||||
key: "storageType",
|
||||
name: "Storage Type",
|
||||
fieldName: "storageType",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
isSorted: true,
|
||||
isSortedDescending: false,
|
||||
showSortIconWhenUnsorted: true,
|
||||
}
|
||||
])
|
||||
|
||||
const [machineSetsList, setMachineSetsList] = useState<IMachineSetsList[]>([])
|
||||
const [machineSetsDetailsVisible, setMachineSetsDetailsVisible] = useState<boolean>(false)
|
||||
const [currentMachine, setCurrentMachine] = useState<string>("")
|
||||
const [shimmerVisibility, SetShimmerVisibility] = useState<boolean>(true)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setMachineSetsList(createMachineSetsList(props.machineSets))
|
||||
}, [props.machineSets] );
|
||||
|
||||
useEffect(() => {
|
||||
const newColumns: IColumn[] = columns.slice();
|
||||
newColumns.forEach(col => {
|
||||
col.onColumnClick = _onColumnClick
|
||||
})
|
||||
setColumns(newColumns)
|
||||
|
||||
if (machineSetsList.length > 0) {
|
||||
SetShimmerVisibility(false)
|
||||
}
|
||||
|
||||
}, [machineSetsList])
|
||||
|
||||
function _onMachineInfoLinkClick(machine: string) {
|
||||
setMachineSetsDetailsVisible(!machineSetsDetailsVisible)
|
||||
setCurrentMachine(machine)
|
||||
}
|
||||
|
||||
function _copyAndSort<T>(items: T[], columnKey: string, isSortedDescending?: boolean): T[] {
|
||||
const key = columnKey as keyof T;
|
||||
return items.slice(0).sort((a: T, b: T) => ((isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1));
|
||||
}
|
||||
|
||||
function _onColumnClick(event: React.MouseEvent<HTMLElement>, column: IColumn): void {
|
||||
let machineLocal: IMachineSetsList[] = machineSetsList;
|
||||
|
||||
let isSortedDescending = column.isSortedDescending;
|
||||
if (column.isSorted) {
|
||||
isSortedDescending = !isSortedDescending;
|
||||
}
|
||||
|
||||
// Sort the items.
|
||||
machineLocal = _copyAndSort(machineLocal, column.fieldName!, isSortedDescending);
|
||||
setMachineSetsList(machineLocal)
|
||||
|
||||
const newColumns: IColumn[] = columns.slice()
|
||||
const currColumn: IColumn = newColumns.filter((currCol) => column.key === currCol.key)[0]
|
||||
|
||||
newColumns.forEach((newCol: IColumn) => {
|
||||
if (newCol === currColumn) {
|
||||
currColumn.isSortedDescending = !currColumn.isSortedDescending
|
||||
currColumn.isSorted = true
|
||||
} else {
|
||||
newCol.isSorted = false
|
||||
newCol.isSortedDescending = true
|
||||
}
|
||||
})
|
||||
|
||||
setColumns(newColumns)
|
||||
}
|
||||
|
||||
function createMachineSetsList(MachineSets: IMachineSet[]): IMachineSetsList[] {
|
||||
return MachineSets.map(machineSet => {
|
||||
return {name: machineSet.name, desiredReplicas: machineSet.desiredReplicas!, currentReplicas: machineSet.replicas!, publicLoadBalancer: machineSet.publicLoadBalancerName, storageType: machineSet.accountStorageType}
|
||||
})
|
||||
}
|
||||
|
||||
const backIconStyles: Partial<IIconStyles> = {
|
||||
root: {
|
||||
height: "100%",
|
||||
width: 40,
|
||||
paddingTop: 5,
|
||||
paddingBottam: 15,
|
||||
svg: {
|
||||
fill: "#e3222f",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const backIconProp = {iconName: "back"}
|
||||
function _onClickBackToMachineList() {
|
||||
setMachineSetsDetailsVisible(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<StackItem>
|
||||
{
|
||||
machineSetsDetailsVisible
|
||||
?
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
<IconButton styles={backIconStyles} onClick={_onClickBackToMachineList} iconProps={backIconProp} />
|
||||
</Stack.Item>
|
||||
<MachineSetsComponent machineSets={props.machineSets} clusterName={props.clusterName} machineSetName={currentMachine}/>
|
||||
</Stack>
|
||||
:
|
||||
<div>
|
||||
<ShimmeredDetailsList
|
||||
setKey="none"
|
||||
items={machineSetsList}
|
||||
columns={columns}
|
||||
enableShimmer={shimmerVisibility}
|
||||
selectionMode={SelectionMode.none}
|
||||
ariaLabelForShimmer="Content is being fetched"
|
||||
ariaLabelForGrid="Item details"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</StackItem>
|
||||
</Stack>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
import { useState, useEffect, useRef } from "react"
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { FetchMachineSets } from '../Request';
|
||||
import { ICluster } from "../App"
|
||||
import { IMessageBarStyles, MessageBar, MessageBarType, Stack } from '@fluentui/react';
|
||||
import { machineSetsKey } from "../ClusterDetail";
|
||||
import { MachineSetsListComponent } from "./MachineSetsList";
|
||||
|
||||
export interface IMachineSet {
|
||||
name?: string,
|
||||
type?: string,
|
||||
createdAt?: string,
|
||||
desiredReplicas?: string,
|
||||
replicas?: string,
|
||||
errorReason?: string,
|
||||
errorMessage?: string
|
||||
publicLoadBalancerName?: string
|
||||
subnet?: string
|
||||
vmSize?: string
|
||||
vNet?: string
|
||||
accountStorageType?: string
|
||||
}
|
||||
|
||||
export interface IOSDisk {
|
||||
diskSettings: string,
|
||||
diskSizeGB: string,
|
||||
managedDisk: IManagedDisk,
|
||||
osType: string
|
||||
}
|
||||
|
||||
export interface IManagedDisk {
|
||||
storageAccountType: string
|
||||
}
|
||||
|
||||
export function MachineSetsWrapper(props: {
|
||||
currentCluster: ICluster
|
||||
detailPanelSelected: string
|
||||
loaded: boolean
|
||||
}) {
|
||||
const [data, setData] = useState<any>([])
|
||||
const [error, setError] = useState<AxiosResponse | null>(null)
|
||||
const state = useRef<MachineSetsListComponent>(null)
|
||||
const [fetching, setFetching] = useState("")
|
||||
|
||||
const errorBarStyles: Partial<IMessageBarStyles> = { root: { marginBottom: 15 } }
|
||||
|
||||
const errorBar = (): any => {
|
||||
return (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.error}
|
||||
isMultiline={false}
|
||||
onDismiss={() => setError(null)}
|
||||
dismissButtonAriaLabel="Close"
|
||||
styles={errorBarStyles}
|
||||
>
|
||||
{error?.statusText}
|
||||
</MessageBar>
|
||||
)
|
||||
}
|
||||
|
||||
// updateData - updates the state of the component
|
||||
// can be used if we want a refresh button.
|
||||
// api/clusterdetail returns a single item.
|
||||
const updateData = (newData: any) => {
|
||||
setData(newData)
|
||||
const machineSetList: IMachineSet[] = []
|
||||
if (state && state.current) {
|
||||
newData.machines.forEach((element: { name: string;
|
||||
type: string;
|
||||
createdat: string;
|
||||
desiredreplicas: number;
|
||||
replicas: number;
|
||||
errorreason: string;
|
||||
errormessage: string;
|
||||
publicloadbalancername: string;
|
||||
subnet: string;
|
||||
accountstoragetype: string;
|
||||
vNet: string;}) => {
|
||||
const machineSet: IMachineSet = {
|
||||
name: element.name,
|
||||
type: element.type,
|
||||
createdAt: element.createdat,
|
||||
desiredReplicas: element.desiredreplicas.toString(),
|
||||
replicas: element.replicas.toString(),
|
||||
errorReason: element.errorreason,
|
||||
errorMessage: element.errormessage,
|
||||
publicLoadBalancerName: element.publicloadbalancername,
|
||||
subnet: element.subnet,
|
||||
vNet: element.vNet,
|
||||
accountStorageType: element.accountstoragetype
|
||||
}
|
||||
|
||||
machineSetList.push(machineSet)
|
||||
});
|
||||
state.current.setState({ machineSets: machineSetList })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const onData = (result: AxiosResponse | null) => {
|
||||
if (result?.status === 200) {
|
||||
updateData(result.data)
|
||||
} else {
|
||||
setError(result)
|
||||
}
|
||||
setFetching(props.currentCluster.name)
|
||||
}
|
||||
|
||||
if (props.detailPanelSelected.toLowerCase() == machineSetsKey &&
|
||||
fetching === "" &&
|
||||
props.loaded &&
|
||||
props.currentCluster.name != "") {
|
||||
setFetching("FETCHING")
|
||||
FetchMachineSets(props.currentCluster).then(onData)
|
||||
}
|
||||
}, [data, props.loaded, props.detailPanelSelected])
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Item grow>{error && errorBar()}</Stack.Item>
|
||||
<Stack>
|
||||
<MachineSetsListComponent machineSets={data!} ref={state} clusterName={props.currentCluster != null ? props.currentCluster.name : ""}/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import { Component } from "react"
|
||||
import { Stack, StackItem, PivotItem, IStackItemStyles, } from '@fluentui/react';
|
||||
import { contentStackStylesNormal } from "../App";
|
||||
import { InfoList } from "./InfoList"
|
||||
import { IMachine } from "./MachinesWrapper";
|
||||
|
||||
interface MachinesComponentProps {
|
||||
machines: any
|
||||
clusterName: string
|
||||
machineName: string
|
||||
}
|
||||
|
||||
const stackItemStyles: IStackItemStyles = {
|
||||
root: {
|
||||
width: "45%",
|
||||
},
|
||||
};
|
||||
|
||||
export const MachineDetails: IMachine = {
|
||||
createdTime: 'Created Time',
|
||||
lastUpdated: "Last Updated",
|
||||
errorReason: "Error Reason",
|
||||
errorMessage: "Error Message",
|
||||
lastOperation: "Last Operation",
|
||||
lastOperationDate: "Last Operation Date",
|
||||
status: "Status"
|
||||
}
|
||||
|
||||
interface IMachinesState {
|
||||
machines: IMachine[],
|
||||
machineName: string,
|
||||
}
|
||||
|
||||
const renderMachines = (machine: IMachine) => {
|
||||
return <PivotItem key={machine.name} headerText={machine.name}>
|
||||
<Stack styles={stackItemStyles}>
|
||||
<StackItem>
|
||||
<InfoList headers={MachineDetails} object={machine} title={machine.name!} titleSize="large"/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</PivotItem>;
|
||||
};
|
||||
|
||||
export class MachinesComponent extends Component<MachinesComponentProps, IMachinesState> {
|
||||
|
||||
constructor(props: MachinesComponentProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
machines: this.props.machines,
|
||||
machineName: this.props.machineName,
|
||||
}
|
||||
}
|
||||
|
||||
private extractCurrentMachine = (machineName: string): IMachine => {
|
||||
this.state.machines.forEach((machine: IMachine) => {
|
||||
if (machine.name === machineName) {
|
||||
return machine
|
||||
}
|
||||
})
|
||||
return this.state.machines[0]
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Stack styles={contentStackStylesNormal}>
|
||||
<Stack>
|
||||
{renderMachines(this.extractCurrentMachine(this.state.machineName))}
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
import * as React from 'react';
|
||||
import { useState, useEffect } from "react"
|
||||
import { Stack, StackItem, IconButton, IIconStyles, SelectionMode } from '@fluentui/react';
|
||||
import { Link } from '@fluentui/react/lib/Link';
|
||||
import { IColumn } from '@fluentui/react/lib/DetailsList';
|
||||
import { ShimmeredDetailsList } from '@fluentui/react/lib/ShimmeredDetailsList';
|
||||
import { IMachine } from "./MachinesWrapper";
|
||||
import { MachinesComponent } from "./Machines"
|
||||
|
||||
|
||||
|
||||
export declare interface IMachinesList {
|
||||
name?: string;
|
||||
status: string;
|
||||
createdTime: string;
|
||||
}
|
||||
|
||||
interface MachinesListComponentProps {
|
||||
machines: any
|
||||
clusterName: string
|
||||
}
|
||||
|
||||
export interface IMachinesListState {
|
||||
machines: IMachine[]
|
||||
clusterName: string
|
||||
}
|
||||
|
||||
export class MachinesListComponent extends React.Component<MachinesListComponentProps, IMachinesListState> {
|
||||
|
||||
constructor(props: MachinesListComponentProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
machines: this.props.machines,
|
||||
clusterName: this.props.clusterName,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<MachinesListHelperComponent machines={this.state.machines} clusterName={this.state.clusterName}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function MachinesListHelperComponent(props: {
|
||||
machines: any,
|
||||
clusterName: string
|
||||
}) {
|
||||
const [columns, setColumns] = useState<IColumn[]>([
|
||||
{
|
||||
key: "machineName",
|
||||
name: "Name",
|
||||
fieldName: "name",
|
||||
minWidth: 150,
|
||||
maxWidth: 350,
|
||||
isResizable: true,
|
||||
isSorted: true,
|
||||
isSortedDescending: false,
|
||||
showSortIconWhenUnsorted: true,
|
||||
onRender: (item: IMachinesList) => (
|
||||
<Link onClick={() => _onMachineInfoLinkClick(item.name!)}>{item.name}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "machineStatus",
|
||||
name: "Status",
|
||||
fieldName: "status",
|
||||
minWidth: 60,
|
||||
maxWidth: 60,
|
||||
isResizable: true,
|
||||
isSorted: true,
|
||||
isSortedDescending: false,
|
||||
showSortIconWhenUnsorted: true,
|
||||
},
|
||||
{
|
||||
key: "createdTime",
|
||||
name: "Created Time",
|
||||
fieldName: "createdTime",
|
||||
minWidth: 120,
|
||||
maxWidth: 150,
|
||||
isResizable: true,
|
||||
isSorted: true,
|
||||
isSortedDescending: false,
|
||||
showSortIconWhenUnsorted: true,
|
||||
}
|
||||
])
|
||||
|
||||
const [machinesList, setMachinesList] = useState<IMachinesList[]>([])
|
||||
const [machinesDetailsVisible, setMachinesDetailsVisible] = useState<boolean>(false)
|
||||
const [currentMachine, setCurrentMachine] = useState<string>("")
|
||||
const [shimmerVisibility, SetShimmerVisibility] = useState<boolean>(true)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setMachinesList(createMachinesList(props.machines))
|
||||
}, [props.machines] );
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const newColumns: IColumn[] = columns.slice();
|
||||
newColumns.forEach(col => {
|
||||
col.onColumnClick = _onColumnClick
|
||||
})
|
||||
setColumns(newColumns)
|
||||
|
||||
if (machinesList.length > 0) {
|
||||
SetShimmerVisibility(false)
|
||||
}
|
||||
|
||||
}, [machinesList])
|
||||
|
||||
function _onMachineInfoLinkClick(machine: string) {
|
||||
setMachinesDetailsVisible(!machinesDetailsVisible)
|
||||
setCurrentMachine(machine)
|
||||
}
|
||||
|
||||
function _copyAndSort<T>(items: T[], columnKey: string, isSortedDescending?: boolean): T[] {
|
||||
const key = columnKey as keyof T;
|
||||
return items.slice(0).sort((a: T, b: T) => ((isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1));
|
||||
}
|
||||
|
||||
function _onColumnClick(event: React.MouseEvent<HTMLElement>, column: IColumn): void {
|
||||
let machineLocal: IMachinesList[] = machinesList;
|
||||
|
||||
let isSortedDescending = column.isSortedDescending;
|
||||
if (column.isSorted) {
|
||||
isSortedDescending = !isSortedDescending;
|
||||
}
|
||||
|
||||
// Sort the items.
|
||||
machineLocal = _copyAndSort(machineLocal, column.fieldName!, isSortedDescending);
|
||||
setMachinesList(machineLocal)
|
||||
|
||||
const newColumns: IColumn[] = columns.slice()
|
||||
const currColumn: IColumn = newColumns.filter((currCol) => column.key === currCol.key)[0]
|
||||
newColumns.forEach((newCol: IColumn) => {
|
||||
if (newCol === currColumn) {
|
||||
currColumn.isSortedDescending = !currColumn.isSortedDescending
|
||||
currColumn.isSorted = true
|
||||
} else {
|
||||
newCol.isSorted = false
|
||||
newCol.isSortedDescending = true
|
||||
}
|
||||
})
|
||||
|
||||
setColumns(newColumns)
|
||||
//setMachinesList(machineLocal)
|
||||
}
|
||||
|
||||
function createMachinesList(machines: IMachine[]): IMachinesList[] {
|
||||
return machines.map(machine => {
|
||||
return {name: machine.name, status: machine.status, createdTime: machine.createdTime}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const backIconStyles: Partial<IIconStyles> = {
|
||||
root: {
|
||||
height: "100%",
|
||||
width: 40,
|
||||
paddingTop: 5,
|
||||
paddingBottam: 15,
|
||||
svg: {
|
||||
fill: "#e3222f",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const backIconProp = {iconName: "back"}
|
||||
function _onClickBackToMachineList() {
|
||||
setMachinesDetailsVisible(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<StackItem>
|
||||
{
|
||||
machinesDetailsVisible
|
||||
?
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
<IconButton styles={backIconStyles} onClick={_onClickBackToMachineList} iconProps={backIconProp} />
|
||||
</Stack.Item>
|
||||
<MachinesComponent machines={props.machines} clusterName={props.clusterName} machineName={currentMachine}/>
|
||||
</Stack>
|
||||
:
|
||||
<div>
|
||||
<ShimmeredDetailsList
|
||||
setKey="none"
|
||||
items={machinesList}
|
||||
columns={columns}
|
||||
selectionMode={SelectionMode.none}
|
||||
enableShimmer={shimmerVisibility}
|
||||
ariaLabelForShimmer="Content is being fetched"
|
||||
ariaLabelForGrid="Item details"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</StackItem>
|
||||
</Stack>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
import { useState, useEffect, useRef } from "react"
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { FetchMachines } from '../Request';
|
||||
import { ICluster } from "../App"
|
||||
import { MachinesListComponent } from './MachinesList';
|
||||
import { IMessageBarStyles, MessageBar, MessageBarType, Stack } from '@fluentui/react';
|
||||
import { machinesKey } from "../ClusterDetail";
|
||||
|
||||
export interface IMachine {
|
||||
name?: string,
|
||||
createdTime: string,
|
||||
lastUpdated: string,
|
||||
errorReason: string,
|
||||
errorMessage: string,
|
||||
lastOperation: string,
|
||||
lastOperationDate: string,
|
||||
status: string
|
||||
}
|
||||
|
||||
export function MachinesWrapper(props: {
|
||||
currentCluster: ICluster
|
||||
detailPanelSelected: string
|
||||
loaded: boolean
|
||||
}) {
|
||||
const [data, setData] = useState<any>([])
|
||||
const [error, setError] = useState<AxiosResponse | null>(null)
|
||||
const state = useRef<MachinesListComponent>(null)
|
||||
const [fetching, setFetching] = useState("")
|
||||
|
||||
const errorBarStyles: Partial<IMessageBarStyles> = { root: { marginBottom: 15 } }
|
||||
|
||||
const errorBar = (): any => {
|
||||
return (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.error}
|
||||
isMultiline={false}
|
||||
onDismiss={() => setError(null)}
|
||||
dismissButtonAriaLabel="Close"
|
||||
styles={errorBarStyles}
|
||||
>
|
||||
{error?.statusText}
|
||||
</MessageBar>
|
||||
)
|
||||
}
|
||||
|
||||
// updateData - updates the state of the component
|
||||
// can be used if we want a refresh button.
|
||||
// api/clusterdetail returns a single item.
|
||||
const updateData = (newData: any) => {
|
||||
setData(newData)
|
||||
const machineList: IMachine[] = []
|
||||
if (state && state.current) {
|
||||
newData.machines.forEach((element: { name: string;
|
||||
createdTime: string;
|
||||
lastUpdated: string;
|
||||
errorReason: string;
|
||||
errorMessage: string;
|
||||
lastOperation: string;
|
||||
lastOperationDate: string;
|
||||
status: string; }) => {
|
||||
const machine: IMachine = {
|
||||
name: element.name,
|
||||
createdTime: element.createdTime,
|
||||
lastUpdated: element.lastUpdated,
|
||||
errorReason: element.errorReason,
|
||||
errorMessage: element.errorMessage,
|
||||
lastOperation: element.lastOperation,
|
||||
lastOperationDate: element.lastOperationDate,
|
||||
status: element.status,
|
||||
}
|
||||
machineList.push(machine)
|
||||
});
|
||||
state.current.setState({ machines: machineList })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const onData = (result: AxiosResponse | null) => {
|
||||
if (result?.status === 200) {
|
||||
updateData(result.data)
|
||||
} else {
|
||||
setError(result)
|
||||
}
|
||||
setFetching(props.currentCluster.name)
|
||||
}
|
||||
|
||||
if (props.detailPanelSelected.toLowerCase() == machinesKey &&
|
||||
fetching === "" &&
|
||||
props.loaded &&
|
||||
props.currentCluster.name != "") {
|
||||
setFetching("FETCHING")
|
||||
FetchMachines(props.currentCluster).then(onData)
|
||||
}
|
||||
}, [data, props.loaded, props.detailPanelSelected])
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Item grow>{error && errorBar()}</Stack.Item>
|
||||
<Stack>
|
||||
<MachinesListComponent machines={data!} ref={state} clusterName={props.currentCluster != null ? props.currentCluster.name : ""} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
import { Component } from "react"
|
||||
import { Stack, Text, StackItem, PivotItem, IStackItemStyles } from '@fluentui/react';
|
||||
import { ICondition, INode, INodeOverviewDetails, IResourceUsage, ITaint, IVolume} from "./NodesWrapper";
|
||||
import { contentStackStylesNormal } from "../App";
|
||||
import { InfoList, MultiInfoList } from "./InfoList"
|
||||
|
||||
export interface NodesComponentProps {
|
||||
nodes: any
|
||||
clusterName: string
|
||||
nodeName: string
|
||||
}
|
||||
|
||||
const stackItemStyles: IStackItemStyles = {
|
||||
root: {
|
||||
width: "45%",
|
||||
},
|
||||
};
|
||||
|
||||
const NodeOverviewDetails: INodeOverviewDetails = {
|
||||
createdTime: 'Created Time'
|
||||
}
|
||||
|
||||
const ResourceDetails: IResourceUsage = {
|
||||
CPU: "CPU",
|
||||
Memory: "Memory",
|
||||
StorageVolume: "Storage Volume",
|
||||
Pods: "Pods"
|
||||
}
|
||||
|
||||
const ConditionDetails: ICondition = {
|
||||
status: "Status",
|
||||
lastHeartbeatTime: "Last Heartbeat Time",
|
||||
lastTransitionTime: "Last Transition Time",
|
||||
message: "Message"
|
||||
}
|
||||
|
||||
const TaintDetails: ITaint = {
|
||||
key: "Key"
|
||||
}
|
||||
|
||||
const VolumeDetails: IVolume = {
|
||||
Path: "Device Path"
|
||||
}
|
||||
|
||||
export interface INodesState {
|
||||
nodes: INode[]
|
||||
clusterName: string
|
||||
nodeName: string
|
||||
}
|
||||
|
||||
const HeadersFromStringMap = (items: Map<string,string>) => {
|
||||
const newItems: any = {}
|
||||
items.forEach((value: string, key: string) => {
|
||||
newItems[key] = key
|
||||
})
|
||||
|
||||
return newItems
|
||||
}
|
||||
|
||||
const ObjectFromStringMap = (items: Map<string,string>) => {
|
||||
const newItems: any = {}
|
||||
items.forEach((value: string, key: string) => {
|
||||
newItems[key] = value
|
||||
})
|
||||
|
||||
return newItems
|
||||
}
|
||||
|
||||
const renderNodes = (node: INode) => {
|
||||
let length = node.capacity.Memory.length
|
||||
node.capacity.Memory = Number(Number(node.capacity.Memory.slice(0, length-2))/1048576).toFixed(2) + "Gi"
|
||||
length = node.capacity.StorageVolume.length
|
||||
node.capacity.StorageVolume = Number(Number(node.capacity.StorageVolume.slice(0, length-2))/1048576).toFixed(2) + "Gi"
|
||||
return <PivotItem key={node.name} headerText={node.name}>
|
||||
<Text variant="xLarge">{node.name}</Text>
|
||||
<Stack horizontal grow>
|
||||
<Stack styles={stackItemStyles}>
|
||||
<StackItem>
|
||||
<InfoList headers={NodeOverviewDetails} object={node} title="Overview" titleSize="large"/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<InfoList headers={ResourceDetails} object={node.capacity} title="Capacity" titleSize="large"/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<InfoList headers={ResourceDetails} object={node.allocatable} title="Allocatable" titleSize="large"/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<InfoList headers={HeadersFromStringMap(node.labels!)} object={ObjectFromStringMap(node.labels!)} title="Labels" titleSize="large"/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Stack styles={stackItemStyles}>
|
||||
<StackItem>
|
||||
<Text variant="large" styles={contentStackStylesNormal}>Conditions</Text>
|
||||
<MultiInfoList headers={ConditionDetails} items={node.conditions} title="Conditions" subProp="type" titleSize="medium"/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Text variant="large" styles={contentStackStylesNormal}>Taints</Text>
|
||||
<MultiInfoList headers={TaintDetails} items={node.taints} title="Taints" subProp="effect" titleSize="medium"/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<InfoList headers={HeadersFromStringMap(node.annotations!)} object={ObjectFromStringMap(node.annotations!)} title="Annotations" titleSize="large"/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
{node.volumes!.length > 0 &&
|
||||
<StackItem>
|
||||
<Text variant="large">Volumes</Text>
|
||||
<MultiInfoList headers={VolumeDetails} items={node.volumes} title="Volumes" subProp="Name" titleSize="medium"/>
|
||||
</StackItem>
|
||||
}
|
||||
</Stack>
|
||||
</PivotItem>;
|
||||
};
|
||||
|
||||
function PivotOverflowMenuExample(props: {
|
||||
nodes: any,
|
||||
nodeName: string
|
||||
}) {
|
||||
let currentNode: INode
|
||||
|
||||
props.nodes.forEach((node: INode) => {
|
||||
if (node.name === props.nodeName) {
|
||||
currentNode = node
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderNodes(currentNode!)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
export class NodesComponent extends Component<NodesComponentProps, INodesState> {
|
||||
|
||||
constructor(props: NodesComponentProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
nodes: this.props.nodes,
|
||||
clusterName: this.props.clusterName,
|
||||
nodeName: this.props.nodeName
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Stack styles={contentStackStylesNormal}>
|
||||
<Stack>
|
||||
<PivotOverflowMenuExample nodes={this.state.nodes} nodeName={this.state.nodeName}/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
import * as React from 'react';
|
||||
import { useState, useEffect } from "react"
|
||||
import { Stack, StackItem, IconButton, IIconStyles, SelectionMode } from '@fluentui/react';
|
||||
import { Link } from '@fluentui/react/lib/Link';
|
||||
import { IColumn } from '@fluentui/react/lib/DetailsList';
|
||||
import { ShimmeredDetailsList } from '@fluentui/react/lib/ShimmeredDetailsList';
|
||||
import { INode } from "./NodesWrapper";
|
||||
import { NodesComponent } from "./Nodes"
|
||||
|
||||
export declare interface INodeList {
|
||||
name: string;
|
||||
status: string;
|
||||
schedulable: string
|
||||
instanceType?: string
|
||||
}
|
||||
|
||||
interface NodeListComponentProps {
|
||||
nodes: any
|
||||
clusterName: string
|
||||
}
|
||||
|
||||
export interface INodeListState {
|
||||
nodes: INode[]
|
||||
clusterName: string
|
||||
}
|
||||
|
||||
export class NodesListComponent extends React.Component<NodeListComponentProps, INodeListState> {
|
||||
|
||||
constructor(props: NodeListComponentProps) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
nodes: this.props.nodes,
|
||||
clusterName: this.props.clusterName,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<NodeListHelperComponent nodes={this.state.nodes} clusterName={this.state.clusterName}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function NodeListHelperComponent(props: {
|
||||
nodes: any,
|
||||
clusterName: string
|
||||
}) {
|
||||
const [columns, setColumns] = useState<IColumn[]>([
|
||||
{
|
||||
key: "nodeName",
|
||||
name: "Name",
|
||||
fieldName: "name",
|
||||
minWidth: 150,
|
||||
maxWidth: 350,
|
||||
isResizable: true,
|
||||
isPadded: true,
|
||||
showSortIconWhenUnsorted: true,
|
||||
isSortedDescending: false,
|
||||
isSorted: true,
|
||||
onRender: (item: INodeList) => (
|
||||
<Link onClick={() => _onNodeInfoLinkClick(item.name)}>{item.name}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "nodeStatus",
|
||||
name: "Status",
|
||||
fieldName: "status",
|
||||
minWidth: 50,
|
||||
maxWidth: 50,
|
||||
isPadded: true,
|
||||
isResizable: true,
|
||||
isSortedDescending: false,
|
||||
isSorted: true,
|
||||
showSortIconWhenUnsorted: true,
|
||||
},
|
||||
{
|
||||
key: "nodeSchedulable",
|
||||
name: "Schedulable",
|
||||
fieldName: "schedulable",
|
||||
minWidth: 70,
|
||||
maxWidth: 70,
|
||||
isPadded: true,
|
||||
isResizable: true,
|
||||
isSortedDescending: false,
|
||||
isSorted: true,
|
||||
showSortIconWhenUnsorted: true,
|
||||
},
|
||||
{
|
||||
key: "nodeInstanceType",
|
||||
name: "Instance Type",
|
||||
fieldName: "instanceType",
|
||||
minWidth: 80,
|
||||
maxWidth: 80,
|
||||
isPadded: true,
|
||||
isResizable: true,
|
||||
isSortedDescending: false,
|
||||
isSorted: true,
|
||||
showSortIconWhenUnsorted: true,
|
||||
}
|
||||
])
|
||||
|
||||
const [nodeList, setNodesList] = useState<INodeList[]>([])
|
||||
const [nodeDetailsVisible, setNodesDetailsVisible] = useState<boolean>(false)
|
||||
const [currentNode, setCurrentNode] = useState<string>("")
|
||||
const [shimmerVisibility, SetShimmerVisibility] = useState<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
setNodesList(createNodeList(props.nodes))
|
||||
}, [props.nodes] );
|
||||
|
||||
useEffect(() => {
|
||||
const newColumns: IColumn[] = columns.slice();
|
||||
newColumns.forEach(col => {
|
||||
col.onColumnClick = _onColumnClick
|
||||
})
|
||||
setColumns(newColumns)
|
||||
|
||||
if (nodeList.length > 0) {
|
||||
SetShimmerVisibility(false)
|
||||
}
|
||||
|
||||
}, [nodeList] )
|
||||
|
||||
function _onNodeInfoLinkClick(node: string) {
|
||||
setNodesDetailsVisible(!nodeDetailsVisible)
|
||||
setCurrentNode(node)
|
||||
}
|
||||
|
||||
function _copyAndSort<T>(items: T[], columnKey: string, isSortedDescending?: boolean): T[] {
|
||||
const key = columnKey as keyof T;
|
||||
return items.slice(0).sort((a: T, b: T) => ((isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1));
|
||||
}
|
||||
|
||||
function _onColumnClick(event: React.MouseEvent<HTMLElement>, column: IColumn): void {
|
||||
let nodeLocal: INodeList[] = nodeList;
|
||||
|
||||
let isSortedDescending = column.isSortedDescending;
|
||||
if (column.isSorted) {
|
||||
isSortedDescending = !isSortedDescending;
|
||||
}
|
||||
|
||||
// Sort the items.
|
||||
nodeLocal = _copyAndSort(nodeLocal, column.fieldName!, isSortedDescending);
|
||||
setNodesList(nodeLocal)
|
||||
|
||||
const newColumns: IColumn[] = columns.slice()
|
||||
const currColumn: IColumn = newColumns.filter((currCol) => column.key === currCol.key)[0]
|
||||
newColumns.forEach((newCol: IColumn) => {
|
||||
if (newCol === currColumn) {
|
||||
currColumn.isSortedDescending = !currColumn.isSortedDescending
|
||||
currColumn.isSorted = true
|
||||
} else {
|
||||
newCol.isSorted = false
|
||||
newCol.isSortedDescending = true
|
||||
}
|
||||
})
|
||||
setColumns(newColumns)
|
||||
}
|
||||
|
||||
function createNodeList(nodes: INode[]): INodeList[] {
|
||||
return nodes.map(node => {
|
||||
let schedulable: string = "True"
|
||||
let instanceType: string = node.labels?.get("node.kubernetes.io/instance-type")!
|
||||
|
||||
if (node.conditions![3].status === "True") {
|
||||
node.taints?.forEach(taint => {
|
||||
schedulable = taint.key === "node.kubernetes.io/unschedulable" ? "False" : "True"
|
||||
})
|
||||
|
||||
return {name: node.name, status: "Ready", schedulable: schedulable!, instanceType: instanceType}
|
||||
} else {
|
||||
schedulable = "--"
|
||||
return {name: node.name, status: "Not Ready", schedulable: schedulable!, instanceType: instanceType}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const backIconStyles: Partial<IIconStyles> = {
|
||||
root: {
|
||||
height: "100%",
|
||||
width: 40,
|
||||
paddingTop: 5,
|
||||
paddingBottam: 15,
|
||||
svg: {
|
||||
fill: "#e3222f",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const backIconProp = {iconName: "back"}
|
||||
function _onClickBackToNodeList() {
|
||||
setNodesDetailsVisible(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<StackItem>
|
||||
{
|
||||
nodeDetailsVisible
|
||||
?
|
||||
<Stack>
|
||||
<Stack.Item>
|
||||
<IconButton styles={backIconStyles} onClick={_onClickBackToNodeList} iconProps={backIconProp} />
|
||||
</Stack.Item>
|
||||
<NodesComponent nodes={props.nodes} clusterName={props.clusterName} nodeName={currentNode}/>
|
||||
</Stack>
|
||||
:
|
||||
<div>
|
||||
<ShimmeredDetailsList
|
||||
setKey="nodeList"
|
||||
items={nodeList}
|
||||
columns={columns}
|
||||
selectionMode={SelectionMode.none}
|
||||
enableShimmer={shimmerVisibility}
|
||||
ariaLabelForShimmer="Content is being fetched"
|
||||
ariaLabelForGrid="Item details"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</StackItem>
|
||||
</Stack>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
import { useState, useEffect, useRef } from "react"
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { FetchNodes } from '../Request';
|
||||
import { ICluster } from "../App"
|
||||
import { NodesListComponent } from './NodesList';
|
||||
import { IMessageBarStyles, MessageBar, MessageBarType, Stack } from '@fluentui/react';
|
||||
import { nodesKey } from "../ClusterDetail";
|
||||
|
||||
export interface ICondition {
|
||||
status: string,
|
||||
lastHeartbeatTime: string,
|
||||
lastTransitionTime: string,
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ITaint {
|
||||
key: string,
|
||||
}
|
||||
|
||||
export interface IVolume {
|
||||
Path: string,
|
||||
}
|
||||
|
||||
export interface IResourceUsage {
|
||||
CPU: string
|
||||
Memory: string
|
||||
StorageVolume: string
|
||||
Pods: string
|
||||
}
|
||||
|
||||
export interface INode {
|
||||
name: string,
|
||||
createdTime: string,
|
||||
capacity: IResourceUsage,
|
||||
allocatable: IResourceUsage
|
||||
conditions?: ICondition[],
|
||||
taints?: ITaint[]
|
||||
labels?: Map<string, string>
|
||||
annotations?: Map<string, string>
|
||||
volumes?: IVolume[]
|
||||
}
|
||||
|
||||
export interface INodeOverviewDetails {
|
||||
createdTime: string
|
||||
}
|
||||
|
||||
export function NodesWrapper(props: {
|
||||
currentCluster: ICluster
|
||||
detailPanelSelected: string
|
||||
loaded: boolean
|
||||
}) {
|
||||
const [data, setData] = useState<any>([])
|
||||
const [error, setError] = useState<AxiosResponse | null>(null)
|
||||
const state = useRef<NodesListComponent>(null)
|
||||
|
||||
const [fetching, setFetching] = useState("")
|
||||
|
||||
const errorBarStyles: Partial<IMessageBarStyles> = { root: { marginBottom: 15 } }
|
||||
|
||||
const errorBar = (): any => {
|
||||
return (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.error}
|
||||
isMultiline={false}
|
||||
onDismiss={() => setError(null)}
|
||||
dismissButtonAriaLabel="Close"
|
||||
styles={errorBarStyles}
|
||||
>
|
||||
{error?.statusText}
|
||||
</MessageBar>
|
||||
)
|
||||
}
|
||||
|
||||
// updateData - updates the state of the component
|
||||
// can be used if we want a refresh button.
|
||||
// api/clusterdetail returns a single item.
|
||||
const updateData = (newData: any) => {
|
||||
setData(newData)
|
||||
const nodeList: INode[] = []
|
||||
if (state && state.current) {
|
||||
newData.nodes.forEach((element: { name: any;
|
||||
createdTime: any;
|
||||
capacity: any;
|
||||
allocatable: any;
|
||||
taints: ITaint[],
|
||||
conditions: ICondition[],
|
||||
labels: Record<string, string>,
|
||||
annotations: Record<string, string>,
|
||||
volumes: IVolume[]}) => {
|
||||
const node: INode = {
|
||||
name: element.name,
|
||||
createdTime: element.createdTime,
|
||||
capacity: element.capacity,
|
||||
allocatable: element.allocatable,
|
||||
}
|
||||
node.taints = []
|
||||
element.taints.forEach((taint: ITaint) => {
|
||||
node.taints!.push(taint)
|
||||
});
|
||||
node.conditions = []
|
||||
element.conditions.forEach((condition: ICondition) => {
|
||||
node.conditions!.push(condition)
|
||||
});
|
||||
node.labels = new Map([])
|
||||
Object.entries(element.labels).forEach((label: [string, string]) => {
|
||||
node.labels?.set(label[0], label[1])
|
||||
});
|
||||
node.volumes = []
|
||||
element.volumes.forEach((volume: IVolume) => {
|
||||
node.volumes!.push(volume)
|
||||
});
|
||||
node.annotations = new Map([])
|
||||
Object.entries(element.annotations).forEach((annotation: [string, string]) => {
|
||||
node.annotations?.set(annotation[0], annotation[1])
|
||||
});
|
||||
nodeList.push(node)
|
||||
});
|
||||
state.current.setState({ nodes: nodeList })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const onData = (result: AxiosResponse | null) => {
|
||||
if (result?.status === 200) {
|
||||
updateData(result.data)
|
||||
} else {
|
||||
setError(result)
|
||||
}
|
||||
setFetching(props.currentCluster.name)
|
||||
}
|
||||
|
||||
if (props.detailPanelSelected.toLowerCase() == nodesKey &&
|
||||
fetching === "" &&
|
||||
props.loaded &&
|
||||
props.currentCluster.name != "") {
|
||||
setFetching("FETCHING")
|
||||
FetchNodes(props.currentCluster).then(onData)
|
||||
}
|
||||
}, [data, props.loaded, props.detailPanelSelected])
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Item grow>{error && errorBar()}</Stack.Item>
|
||||
<Stack>
|
||||
<NodesListComponent nodes={data!} ref={state} clusterName={props.currentCluster != null ? props.currentCluster.name : ""} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
import { IShimmerStyles, Shimmer, ShimmerElementType } from '@fluentui/react/lib/Shimmer';
|
||||
import { Component } from "react"
|
||||
import { Stack, Text, IStackStyles, IStackItemStyles } from '@fluentui/react';
|
||||
import { contentStackStylesNormal } from "../App"
|
||||
import { IClusterDetails } from "../ClusterDetailList"
|
||||
|
||||
interface OverviewComponentProps {
|
||||
item: any
|
||||
clusterName: string
|
||||
}
|
||||
|
||||
interface IOverviewComponentState {
|
||||
item: IClusterDetails
|
||||
}
|
||||
|
||||
export const ShimmerStyle: Partial<IShimmerStyles> = {
|
||||
root: {
|
||||
margin: "11px 0"
|
||||
}
|
||||
}
|
||||
|
||||
export const headShimmerStyle: Partial<IShimmerStyles> = {
|
||||
root: {
|
||||
margin: "15px 0"
|
||||
}
|
||||
}
|
||||
|
||||
export const headerShimmer = [
|
||||
{ type: ShimmerElementType.line, height: 32, width: '25%' },
|
||||
]
|
||||
|
||||
export const rowShimmer = [
|
||||
{ type: ShimmerElementType.line, height: 18, width: '75%' },
|
||||
]
|
||||
|
||||
export const KeyColumnStyle: Partial<IStackStyles> = {
|
||||
root: {
|
||||
paddingTop: 10,
|
||||
paddingRight: 15,
|
||||
}
|
||||
}
|
||||
|
||||
export const ValueColumnStyle: Partial<IStackStyles> = {
|
||||
root: {
|
||||
paddingTop: 10,
|
||||
}
|
||||
}
|
||||
|
||||
export const KeyStyle: IStackItemStyles = {
|
||||
root: {
|
||||
fontStyle: "bold",
|
||||
alignSelf: "flex-start",
|
||||
fontVariantAlternates: "bold",
|
||||
color: "grey",
|
||||
paddingBottom: 10
|
||||
}
|
||||
}
|
||||
|
||||
export const ValueStyle: IStackItemStyles = {
|
||||
root: {
|
||||
paddingBottom: 10
|
||||
}
|
||||
}
|
||||
|
||||
const clusterDetailHeadings : IClusterDetails = {
|
||||
apiServerVisibility: 'ApiServer Visibility',
|
||||
apiServerURL: 'ApiServer URL',
|
||||
architectureVersion: 'Architecture Version',
|
||||
consoleLink: 'Console Link',
|
||||
createdAt: 'Created At',
|
||||
createdBy: 'Created By',
|
||||
failedProvisioningState: 'Failed Provisioning State',
|
||||
infraId: 'Infra Id',
|
||||
lastAdminUpdateError: 'Last Admin Update Error',
|
||||
lastModifiedAt: 'Last Modified At',
|
||||
lastModifiedBy: 'Last Modified By',
|
||||
lastProvisioningState: 'Last Provisioning State',
|
||||
location: 'Location',
|
||||
name: 'Name',
|
||||
provisioningState: 'Provisioning State',
|
||||
resourceId: 'Resource Id',
|
||||
version: 'Version',
|
||||
installStatus: 'Installation Status'
|
||||
}
|
||||
|
||||
function ClusterDetailCell(
|
||||
value: any,
|
||||
): any {
|
||||
if (typeof (value.value) == typeof (" ")) {
|
||||
return <Stack.Item id="ClusterDetailCell" styles={value.style}>
|
||||
<Text styles={value.style} variant={'medium'}>{value.value}</Text>
|
||||
</Stack.Item>
|
||||
}
|
||||
}
|
||||
|
||||
export class OverviewComponent extends Component<OverviewComponentProps, IOverviewComponentState> {
|
||||
|
||||
constructor(props: OverviewComponentProps | Readonly<OverviewComponentProps>) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const headerEntries = Object.entries(clusterDetailHeadings)
|
||||
const filteredHeaders: Array<[string, any]> = []
|
||||
if (this.props.item.length != 0) {
|
||||
headerEntries.filter((element: [string, any]) => {
|
||||
if (this.props.item[element[0]] != null &&
|
||||
this.props.item[element[0]].toString().length > 0) {
|
||||
filteredHeaders.push(element)
|
||||
}
|
||||
})
|
||||
return (
|
||||
<Stack styles={contentStackStylesNormal}>
|
||||
<Stack horizontal>
|
||||
<Stack id="Headers" styles={KeyColumnStyle}>
|
||||
{filteredHeaders.map((value: [string, any], index: number) => (
|
||||
<ClusterDetailCell style={KeyStyle} key={index} value={value[1]} />
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack id="Colons" styles={KeyColumnStyle}>
|
||||
{Array(filteredHeaders.length).fill(':').map((value: [string], index: number) => (
|
||||
<ClusterDetailCell style={KeyStyle} key={index} value={value} />
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack id="Values" styles={ValueColumnStyle}>
|
||||
{filteredHeaders.map((value: [string, any], index: number) => (
|
||||
<ClusterDetailCell style={ValueStyle}
|
||||
key={index}
|
||||
value={this.props.item[value[0]]} />
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Stack>
|
||||
<Shimmer styles={headShimmerStyle} shimmerElements={headerShimmer} width="25%"></Shimmer>
|
||||
{headerEntries.map(header => (
|
||||
<Shimmer key={header[0]} styles={ShimmerStyle} shimmerElements={rowShimmer} width="75%"></Shimmer>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { useState, useEffect, useRef } from "react"
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { FetchClusterInfo } from '../Request';
|
||||
import { ICluster } from "../App"
|
||||
import { ClusterDetailComponent } from '../ClusterDetailList'
|
||||
import { OverviewComponent } from './Overview';
|
||||
import { IMessageBarStyles, MessageBar, MessageBarType, Stack } from '@fluentui/react';
|
||||
import { overviewKey } from "../ClusterDetail";
|
||||
|
||||
const errorBarStyles: Partial<IMessageBarStyles> = { root: { marginBottom: 15 } }
|
||||
|
||||
export function OverviewWrapper(props: {
|
||||
clusterName: string
|
||||
currentCluster: ICluster
|
||||
detailPanelSelected: string
|
||||
loaded: boolean
|
||||
}) {
|
||||
const [data, setData] = useState<any>([])
|
||||
const [error, setError] = useState<AxiosResponse | null>(null)
|
||||
const state = useRef<ClusterDetailComponent>(null)
|
||||
const [fetching, setFetching] = useState("")
|
||||
|
||||
const errorBar = (): any => {
|
||||
return (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.error}
|
||||
isMultiline={false}
|
||||
onDismiss={() => setError(null)}
|
||||
dismissButtonAriaLabel="Close"
|
||||
styles={errorBarStyles}
|
||||
>
|
||||
{error?.statusText}
|
||||
</MessageBar>
|
||||
)
|
||||
}
|
||||
|
||||
// updateData - updates the state of the component
|
||||
// can be used if we want a refresh button.
|
||||
// api/clusterdetail returns a single item.
|
||||
const updateData = (newData: any) => {
|
||||
setData(newData)
|
||||
if (state && state.current) {
|
||||
state.current.setState({ item: newData, detailPanelSelected: props.detailPanelSelected })
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const onData = (result: AxiosResponse | null) => {
|
||||
if (result?.status === 200) {
|
||||
updateData(result.data)
|
||||
} else {
|
||||
setError(result)
|
||||
}
|
||||
setFetching(props.currentCluster.name)
|
||||
}
|
||||
|
||||
if (props.detailPanelSelected.toLowerCase() == overviewKey &&
|
||||
fetching === "" &&
|
||||
props.loaded &&
|
||||
props.currentCluster.name != "") {
|
||||
setFetching("FETCHING")
|
||||
FetchClusterInfo(props.currentCluster).then(onData)
|
||||
}
|
||||
}, [data, props.loaded, props.clusterName])
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Item grow>{error && errorBar()}</Stack.Item>
|
||||
<Stack>
|
||||
<OverviewComponent item={data} clusterName={props.currentCluster != null ? props.currentCluster.name : ""}/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
|
@ -255,6 +255,20 @@ class ClusterListComponent extends Component<ClusterListComponentProps, ICluster
|
|||
/>
|
||||
</TooltipHost>
|
||||
<KubeconfigButton resourceId={item.resourceId} csrfToken={props.csrfToken} />
|
||||
{/* <TooltipHost content={`Geneva`}>
|
||||
<IconButton
|
||||
iconProps={{iconName: "Health"}}
|
||||
aria-label="Geneva"
|
||||
href={item.resourceId + `/geneva`}
|
||||
/>
|
||||
</TooltipHost>
|
||||
<TooltipHost content={`Feature Flags`}>
|
||||
<IconButton
|
||||
iconProps={{iconName: "IconSetsFlag"}}
|
||||
aria-label="featureFlags"
|
||||
href={item.resourceId + `/feature-flags`}
|
||||
/>
|
||||
</TooltipHost> */}
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
|
|
|
@ -15,7 +15,7 @@ export const FetchClusters = async (): Promise<AxiosResponse | null> => {
|
|||
const result = await axios("/api/clusters")
|
||||
return result
|
||||
} catch (e: any) {
|
||||
let err = e.response as AxiosResponse
|
||||
const err = e.response as AxiosResponse
|
||||
return OnError(err)
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ export const FetchClusterInfo = async (cluster: ICluster): Promise<AxiosResponse
|
|||
)
|
||||
return result
|
||||
} catch (e: any) {
|
||||
let err = e.response as AxiosResponse
|
||||
const err = e.response as AxiosResponse
|
||||
return OnError(err)
|
||||
}
|
||||
}
|
||||
|
@ -37,17 +37,50 @@ export const FetchInfo = async (): Promise<AxiosResponse | null> => {
|
|||
const result = await axios("/api/info")
|
||||
return result
|
||||
} catch (e: any) {
|
||||
let err = e.response as AxiosResponse
|
||||
const err = e.response as AxiosResponse
|
||||
return OnError(err)
|
||||
}
|
||||
}
|
||||
|
||||
export const FetchNodes = async (cluster: ICluster): Promise<AxiosResponse | null> => {
|
||||
try {
|
||||
const result = await axios(
|
||||
"/api/" + cluster.subscription + "/" + cluster.resourceGroup + "/" + cluster.name + "/nodes")
|
||||
return result
|
||||
} catch (e: any) {
|
||||
const err = e.response as AxiosResponse
|
||||
return OnError(err)
|
||||
}
|
||||
}
|
||||
|
||||
export const FetchMachines = async (cluster: ICluster): Promise<AxiosResponse | null> => {
|
||||
try {
|
||||
const result = await axios(
|
||||
"/api/" + cluster.subscription + "/" + cluster.resourceGroup + "/" + cluster.name + "/machines")
|
||||
return result
|
||||
} catch (e: any) {
|
||||
const err = e.response as AxiosResponse
|
||||
return OnError(err)
|
||||
}
|
||||
}
|
||||
|
||||
export const FetchMachineSets = async (cluster: ICluster): Promise<AxiosResponse | null> => {
|
||||
try {
|
||||
const result = await axios(
|
||||
"/api/" + cluster.subscription + "/" + cluster.resourceGroup + "/" + cluster.name + "/machine-sets")
|
||||
return result
|
||||
} catch (e: any) {
|
||||
const err = e.response as AxiosResponse
|
||||
return OnError(err)
|
||||
}
|
||||
}
|
||||
|
||||
export const ProcessLogOut = async (): Promise<any> => {
|
||||
try {
|
||||
const result = await axios({ method: "POST", url: "/api/logout" })
|
||||
const result = await axios({method: "POST", url: "/api/logout"})
|
||||
return result
|
||||
} catch (e: any) {
|
||||
let err = e.response as AxiosResponse
|
||||
const err = e.response as AxiosResponse
|
||||
console.log(err)
|
||||
}
|
||||
document.location.href = "/api/login"
|
||||
|
@ -67,7 +100,7 @@ export const RequestKubeconfig = async (
|
|||
})
|
||||
return result
|
||||
} catch (e: any) {
|
||||
let err = e.response as AxiosResponse
|
||||
const err = e.response as AxiosResponse
|
||||
return OnError(err)
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче