Backporting old admin portal changes (#2137)

Co-authored-by: Ellis Johnson <elljohns@redhat.com>
This commit is contained in:
Anshul Verma 2022-11-10 12:59:16 +05:30 коммит произвёл GitHub
Родитель 93663d1d35
Коммит cf9c5210b4
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
46 изменённых файлов: 14523 добавлений и 2592 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -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"
@ -37,7 +42,9 @@ type client struct {
// structures. The concrete implementation of FetchClient wraps this.
type realFetcher struct {
log *logrus.Entry
configcli configclient.Interface
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,
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)
}
}

145
pkg/portal/cluster/nodes.go Normal file
Просмотреть файл

@ -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>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

8775
portal/v2/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -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 (
<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>
<OverviewWrapper clusterName= {this.props.item.name} currentCluster={this.props.cluster!} detailPanelSelected={this.props.detailPanelVisible} loaded={this.props.isDataLoaded}/>
)
}
case "nodes":
{
return (
<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)
}
}