Merge pull request #2554 from anshulvermapatel/azure-side-information

Adding VM's allocation status column to the machines table
This commit is contained in:
Ellis Johnson 2023-03-22 15:00:01 +11:00 коммит произвёл GitHub
Родитель 3a874e3c90 f0f93961be
Коммит eb1b604233
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
21 изменённых файлов: 513 добавлений и 58 удалений

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

@ -26,7 +26,8 @@ import (
)
func portal(ctx context.Context, log *logrus.Entry, audit *logrus.Entry) error {
_env, err := env.NewCore(ctx, log)
_env, err := env.NewEnv(ctx, log)
if err != nil {
return err
}
@ -115,6 +116,11 @@ func portal(ctx context.Context, log *logrus.Entry, audit *logrus.Entry) error {
return err
}
dbSubscriptions, err := database.NewSubscriptions(ctx, _env.IsLocalDevelopmentMode(), dbc)
if err != nil {
return err
}
portalKeyvaultURI, err := keyvault.URI(_env, env.PortalKeyvaultSuffix)
if err != nil {
return err
@ -187,7 +193,7 @@ func portal(ctx context.Context, log *logrus.Entry, audit *logrus.Entry) error {
log.Printf("listening %s", address)
p := pkgportal.NewPortal(_env, audit, log.WithField("component", "portal"), log.WithField("component", "portal-access"), l, sshl, verifier, hostname, servingKey, servingCerts, clientID, clientKey, clientCerts, sessionKey, sshKey, groupIDs, elevatedGroupIDs, dbOpenShiftClusters, dbPortal, dialer, m)
p := pkgportal.NewPortal(_env, audit, log.WithField("component", "portal"), log.WithField("component", "portal-access"), l, sshl, verifier, hostname, servingKey, servingCerts, clientID, clientKey, clientCerts, sessionKey, sshKey, groupIDs, elevatedGroupIDs, dbOpenShiftClusters, dbPortal, dbSubscriptions, dialer, m)
return p.Run(ctx)
}

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

@ -1,10 +1,10 @@
{
"files": {
"main.js": "/static/js/main.61fbb3a0.js",
"main.js": "/static/js/main.ae7987cb.js",
"index.html": "/index.html",
"main.61fbb3a0.js.map": "/static/js/main.61fbb3a0.js.map"
"main.ae7987cb.js.map": "/static/js/main.ae7987cb.js.map"
},
"entrypoints": [
"static/js/main.61fbb3a0.js"
"static/js/main.ae7987cb.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.61fbb3a0.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.ae7987cb.js"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

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

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

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

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

@ -88,7 +88,11 @@ func (p *portal) clusters(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(b)
_, err = w.Write(b)
if err != nil {
p.internalServerError(w, err)
return
}
}
func (p *portal) clusterOperators(w http.ResponseWriter, r *http.Request) {
@ -113,7 +117,11 @@ func (p *portal) clusterOperators(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(b)
_, err = w.Write(b)
if err != nil {
p.internalServerError(w, err)
return
}
}
func (p *portal) nodes(w http.ResponseWriter, r *http.Request) {
@ -138,7 +146,11 @@ func (p *portal) nodes(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(b)
_, err = w.Write(b)
if err != nil {
p.internalServerError(w, err)
return
}
}
func (p *portal) machines(w http.ResponseWriter, r *http.Request) {
@ -163,7 +175,40 @@ func (p *portal) machines(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(b)
_, err = w.Write(b)
if err != nil {
p.internalServerError(w, err)
return
}
}
func (p *portal) VMAllocationStatus(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
azurefetcher, err := p.makeAzureFetcher(ctx, r)
if err != nil {
p.internalServerError(w, err)
return
}
machineVMAllocationStatus, err := azurefetcher.VMAllocationStatus(ctx)
if err != nil {
p.internalServerError(w, err)
return
}
b, err := json.MarshalIndent(machineVMAllocationStatus, "", " ")
if err != nil {
p.internalServerError(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(b)
if err != nil {
p.internalServerError(w, err)
return
}
}
func (p *portal) machineSets(w http.ResponseWriter, r *http.Request) {
@ -188,5 +233,9 @@ func (p *portal) machineSets(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(b)
_, err = w.Write(b)
if err != nil {
p.internalServerError(w, err)
return
}
}

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

@ -6,16 +6,30 @@ package cluster
import (
"context"
"github.com/Azure/go-autorest/autorest"
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/env"
"github.com/Azure/ARO-RP/pkg/proxy"
"github.com/Azure/ARO-RP/pkg/util/azureclient"
"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/compute"
"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/features"
"github.com/Azure/ARO-RP/pkg/util/restconfig"
"github.com/Azure/ARO-RP/pkg/util/stringutils"
)
type ResourceClientFactory interface {
NewResourcesClient(environment *azureclient.AROEnvironment, subscriptionID string, authorizer autorest.Authorizer) features.ResourcesClient
}
type VirtualMachinesClientFactory interface {
NewVirtualMachinesClient(environment *azureclient.AROEnvironment, subscriptionID string, authorizer autorest.Authorizer) compute.VirtualMachinesClient
}
// FetchClient is the interface that the Admin Portal Frontend uses to gather
// information about clusters. It returns frontend-suitable data structures.
type FetchClient interface {
@ -25,6 +39,10 @@ type FetchClient interface {
MachineSets(context.Context) (*MachineSetListInformation, error)
}
type AzureFetchClient interface {
VMAllocationStatus(context.Context) (map[string]string, error)
}
// client is an implementation of FetchClient. It currently contains a "fetcher"
// which is responsible for fetching information from the k8s clusters. The
// mechanism of fetching the data from the cluster and returning it to the
@ -47,6 +65,45 @@ type realFetcher struct {
machineClient machineclient.Interface
}
// azureClient is the same implementation as client's, the only difference is that it will be used to fetch something from azure regarding a cluster.
type azureClient struct {
log *logrus.Entry
fetcher *azureSideFetcher
}
// azureSideFetcher is responsible for fetching information about azure resources of a k8s cluster. It
// contains azure related authentication/authorization data and returns the frontend-suitable data
// structures. The concrete implementation of AzureFetchClient wraps this.
type azureSideFetcher struct {
log *logrus.Entry
resourceGroupName string
subscriptionDoc *api.SubscriptionDocument
env env.Interface
resourceClientFactory ResourceClientFactory
virtualMachinesClientFactory VirtualMachinesClientFactory
}
type clientFactory struct{}
func (cf clientFactory) NewResourcesClient(environment *azureclient.AROEnvironment, subscriptionID string, authorizer autorest.Authorizer) features.ResourcesClient {
return features.NewResourcesClient(environment, subscriptionID, authorizer)
}
func (cf clientFactory) NewVirtualMachinesClient(environment *azureclient.AROEnvironment, subscriptionID string, authorizer autorest.Authorizer) compute.VirtualMachinesClient {
return compute.NewVirtualMachinesClient(environment, subscriptionID, authorizer)
}
func newAzureSideFetcher(log *logrus.Entry, resourceGroupName string, subscriptionDoc *api.SubscriptionDocument, env env.Interface, resourceClientFactory ResourceClientFactory, virtualMachinesClientFactory VirtualMachinesClientFactory) azureSideFetcher {
return azureSideFetcher{
log: log,
resourceGroupName: resourceGroupName,
subscriptionDoc: subscriptionDoc,
env: env,
resourceClientFactory: resourceClientFactory,
virtualMachinesClientFactory: virtualMachinesClientFactory,
}
}
func newRealFetcher(log *logrus.Entry, dialer proxy.Dialer, doc *api.OpenShiftClusterDocument) (*realFetcher, error) {
restConfig, err := restconfig.RestConfig(dialer, doc.OpenShiftCluster)
if err != nil {
@ -91,3 +148,13 @@ func NewFetchClient(log *logrus.Entry, dialer proxy.Dialer, cluster *api.OpenShi
fetcher: fetcher,
}, nil
}
func NewAzureFetchClient(log *logrus.Entry, cluster *api.OpenShiftClusterDocument, subscriptionDoc *api.SubscriptionDocument, env env.Interface) AzureFetchClient {
resourceGroupName := stringutils.LastTokenByte(cluster.OpenShiftCluster.Properties.ClusterProfile.ResourceGroupID, '/')
cf := clientFactory{}
azureSideFetcher := newAzureSideFetcher(log, resourceGroupName, subscriptionDoc, env, cf, cf)
return &azureClient{
log: log,
fetcher: &azureSideFetcher,
}
}

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

@ -5,9 +5,15 @@ package cluster
import (
"context"
"strings"
mgmtcompute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-01/compute"
mgmtfeatures "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-07-01/features"
machinev1beta1 "github.com/openshift/api/machine/v1beta1"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/compute"
)
type MachinesInformation struct {
@ -59,7 +65,52 @@ func (c *client) Machines(ctx context.Context) (*MachineListInformation, error)
return c.fetcher.Machines(ctx)
}
func (c *azureClient) VMAllocationStatus(ctx context.Context) (map[string]string, error) {
return c.fetcher.vmAllocationStatus(ctx)
}
func (f *azureSideFetcher) vmAllocationStatus(ctx context.Context) (map[string]string, error) {
env := f.env
subscriptionDoc := f.subscriptionDoc
clusterRGName := f.resourceGroupName
aroEnvironment := env.Environment()
fpAuth, err := env.FPAuthorizer(subscriptionDoc.Subscription.Properties.TenantID, aroEnvironment.ResourceManagerEndpoint)
if err != nil {
return nil, err
}
// Getting Virtual Machine resources through the Cluster's Resource Group
computeResources, err := f.resourceClientFactory.NewResourcesClient(aroEnvironment, subscriptionDoc.ID, fpAuth).ListByResourceGroup(ctx, clusterRGName, "resourceType eq 'Microsoft.Compute/virtualMachines'", "", nil)
if err != nil {
return nil, err
}
vmAllocationStatus := make(map[string]string)
virtualMachineClient := f.virtualMachinesClientFactory.NewVirtualMachinesClient(aroEnvironment, subscriptionDoc.ID, fpAuth)
for _, res := range computeResources {
putAllocationStatusToMap(ctx, clusterRGName, vmAllocationStatus, res, virtualMachineClient, f.log)
}
return vmAllocationStatus, nil
}
// Helper Functions
func putAllocationStatusToMap(ctx context.Context, clusterRGName string, vmAllocationStatus map[string]string, res mgmtfeatures.GenericResourceExpanded, virtualMachineClient compute.VirtualMachinesClient, log *logrus.Entry) {
vm, err := virtualMachineClient.Get(ctx, clusterRGName, *res.Name, mgmtcompute.InstanceView)
if err != nil {
log.Warn(err) // can happen when the ARM cache is lagging
return
}
vmName := *vm.Name
instanceViewStatuses := vm.InstanceView.Statuses
for _, status := range *instanceViewStatuses {
if strings.HasPrefix(*status.Code, "PowerState/") {
vmAllocationStatus[vmName] = *status.Code
return
}
}
vmAllocationStatus[vmName] = ""
}
func getLastOperation(machine machinev1beta1.Machine) string {
lastOperation := "Unknown"
if machine.Status.LastOperation != nil &&

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

@ -6,15 +6,31 @@ package cluster
import (
"context"
"encoding/json"
"errors"
"reflect"
"regexp"
"sort"
"testing"
mgmtcompute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-01/compute"
mgmtfeatures "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2019-07-01/features"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/go-test/deep"
"github.com/golang/mock/gomock"
machinev1beta1 "github.com/openshift/api/machine/v1beta1"
machinefake "github.com/openshift/client-go/machine/clientset/versioned/fake"
kruntime "k8s.io/apimachinery/pkg/runtime"
"github.com/Azure/ARO-RP/pkg/api"
"github.com/Azure/ARO-RP/pkg/util/azureclient"
"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/compute"
"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/features"
mock_compute "github.com/Azure/ARO-RP/pkg/util/mocks/azureclient/mgmt/compute"
mock_features "github.com/Azure/ARO-RP/pkg/util/mocks/azureclient/mgmt/features"
mock_env "github.com/Azure/ARO-RP/pkg/util/mocks/env"
mock_refreshable "github.com/Azure/ARO-RP/pkg/util/mocks/refreshable"
"github.com/Azure/ARO-RP/pkg/util/refreshable"
testlog "github.com/Azure/ARO-RP/test/util/log"
)
@ -97,3 +113,133 @@ func TestMachines(t *testing.T) {
t.Fatal(r)
}
}
type MockClientFactory struct {
mockResourcesClient *mock_features.MockResourcesClient
mockVirtualMachinesClient *mock_compute.MockVirtualMachinesClient
}
func newMockClientFactory(mockResourcesClient *mock_features.MockResourcesClient, mockVirtualMachinesClient *mock_compute.MockVirtualMachinesClient) MockClientFactory {
return MockClientFactory{mockResourcesClient: mockResourcesClient, mockVirtualMachinesClient: mockVirtualMachinesClient}
}
func (mcf MockClientFactory) NewResourcesClient(environment *azureclient.AROEnvironment, subscriptionID string, authorizer autorest.Authorizer) features.ResourcesClient {
return mcf.mockResourcesClient
}
func (mcf MockClientFactory) NewVirtualMachinesClient(environment *azureclient.AROEnvironment, subscriptionID string, authorizer autorest.Authorizer) compute.VirtualMachinesClient {
return mcf.mockVirtualMachinesClient
}
func TestVMAllocationStatus(t *testing.T) {
ctx := context.Background()
subscriptionDoc := &api.SubscriptionDocument{
ID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
ResourceID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
Timestamp: 1668689726,
Self: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
ETag: "\"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\"",
Attachments: "attachments/",
Subscription: &api.Subscription{
State: "Registered",
Properties: &api.SubscriptionProperties{
TenantID: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
RegisteredFeatures: []api.RegisteredFeatureProfile{
{Name: "Microsoft.RedHatOpenShift/RedHatEngineering", State: "Registered"},
},
},
},
}
controller := gomock.NewController(t)
mockEnv := mock_env.NewMockInterface(controller)
mockRefreshable := mock_refreshable.NewMockAuthorizer(controller)
mockResourcesClient := mock_features.NewMockResourcesClient(controller)
mockVirtualMachinesClient := mock_compute.NewMockVirtualMachinesClient(controller)
type test struct {
name string
returnedRefreshableAuthorizer refreshable.Authorizer
returnedVM mgmtcompute.VirtualMachine
returnedGenericResourceExpanded []mgmtfeatures.GenericResourceExpanded
wantOutput map[string]string
wantErr error
}
for _, tt := range []*test{
{
name: "Successfully fetching VMs allocation status. Calling all the required methods.",
returnedRefreshableAuthorizer: mockRefreshable,
returnedGenericResourceExpanded: []mgmtfeatures.GenericResourceExpanded{
{
Kind: func(v string) *string { return &v }("something"),
Type: func(v string) *string { return &v }("Microsoft.Compute/virtualMachines"),
Name: func(v string) *string { return &v }("master-x"),
},
},
returnedVM: mgmtcompute.VirtualMachine{
Name: func(v string) *string { return &v }("master-x"),
VirtualMachineProperties: &mgmtcompute.VirtualMachineProperties{
InstanceView: &mgmtcompute.VirtualMachineInstanceView{
Statuses: &[]mgmtcompute.InstanceViewStatus{
{
Code: func() *string {
s := new(string)
*s = "PowerState/running"
return s
}(),
},
},
},
},
},
wantOutput: map[string]string{"master-x": "PowerState/running"},
wantErr: nil,
},
{
name: "No VM resource found",
returnedRefreshableAuthorizer: mockRefreshable,
returnedGenericResourceExpanded: []mgmtfeatures.GenericResourceExpanded{},
wantOutput: map[string]string{},
wantErr: nil,
},
{
name: "Empty FP Authorizer",
returnedRefreshableAuthorizer: nil,
returnedGenericResourceExpanded: nil,
wantOutput: nil,
wantErr: errors.New("Empty Athorizer"),
},
} {
t.Run(tt.name, func(t *testing.T) {
mockEnv.EXPECT().Environment().Return(&azureclient.AROEnvironment{
Environment: azure.Environment{
ResourceManagerEndpoint: "temp",
},
})
mockEnv.EXPECT().FPAuthorizer(gomock.Any(), gomock.Any()).Return(tt.returnedRefreshableAuthorizer, tt.wantErr)
if tt.returnedRefreshableAuthorizer != nil {
mockResourcesClient.EXPECT().ListByResourceGroup(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(tt.returnedGenericResourceExpanded, tt.wantErr)
}
if len(tt.returnedGenericResourceExpanded) > 0 {
mockVirtualMachinesClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(tt.returnedVM, tt.wantErr)
}
_, log := testlog.New()
mcf := newMockClientFactory(mockResourcesClient, mockVirtualMachinesClient)
azureSideFetcher := &azureSideFetcher{
log: log,
resourceGroupName: "someResourceGroup",
subscriptionDoc: subscriptionDoc,
env: mockEnv,
resourceClientFactory: mcf,
virtualMachinesClientFactory: mcf,
}
azureClient := &azureClient{fetcher: azureSideFetcher, log: log}
vmAllocationStatuses, err := azureClient.VMAllocationStatus(ctx)
if !reflect.DeepEqual(vmAllocationStatuses, tt.wantOutput) {
t.Error("Expected output", tt.wantOutput, "Got", vmAllocationStatuses)
}
if err != nil && err != tt.wantErr || err == nil && tt.wantErr != nil {
t.Error("Expected", tt.wantErr, "Got", err)
}
})
}
}

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

@ -27,13 +27,13 @@ type testPortal struct {
portalLogHook *test.Hook
}
func NewTestPortal(_env env.Core, dbOpenShiftClusters database.OpenShiftClusters, dbPortal database.Portal) *testPortal {
func NewTestPortal(_env env.Interface, dbOpenShiftClusters database.OpenShiftClusters, dbPortal database.Portal, dbSubscription database.Subscriptions) *testPortal {
_, portalAccessLog := testlog.New()
portalLogHook, portalLog := testlog.New()
auditHook, portalAuditLog := testlog.NewAudit()
l := listener.NewListener()
p := NewPortal(_env, portalAuditLog, portalLog, portalAccessLog, l, nil, nil, "", nil, nil, "", nil, nil, make([]byte, 32), nil, nonElevatedGroupIDs, elevatedGroupIDs, dbOpenShiftClusters, dbPortal, nil, nil).(*portal)
p := NewPortal(_env, portalAuditLog, portalLog, portalAccessLog, l, nil, nil, "", nil, nil, "", nil, nil, make([]byte, 32), nil, nonElevatedGroupIDs, elevatedGroupIDs, dbOpenShiftClusters, dbPortal, dbSubscription, nil, nil).(*portal)
return &testPortal{
p: p,

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

@ -23,7 +23,7 @@ func TestInfo(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
_env := mock_env.NewMockCore(controller)
_env := mock_env.NewMockInterface(controller)
_env.EXPECT().IsLocalDevelopmentMode().AnyTimes().Return(false)
_env.EXPECT().Location().AnyTimes().Return("eastus")
_env.EXPECT().TenantID().AnyTimes().Return("00000000-0000-0000-0000-000000000001")
@ -32,8 +32,9 @@ func TestInfo(t *testing.T) {
dbOpenShiftClusters, _ := testdatabase.NewFakeOpenShiftClusters()
dbPortal, _ := testdatabase.NewFakePortal()
dbSubscription, _ := testdatabase.NewFakeSubscriptions()
p := NewTestPortal(_env, dbOpenShiftClusters, dbPortal)
p := NewTestPortal(_env, dbOpenShiftClusters, dbPortal, dbSubscription)
defer p.Cleanup()
err := p.Run(ctx)
if err != nil {

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

@ -19,12 +19,15 @@ import (
"strings"
"time"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"github.com/Azure/ARO-RP/pkg/api"
"github.com/Azure/ARO-RP/pkg/api/validate"
"github.com/Azure/ARO-RP/pkg/database"
"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
"github.com/Azure/ARO-RP/pkg/env"
frontendmiddleware "github.com/Azure/ARO-RP/pkg/frontend/middleware"
"github.com/Azure/ARO-RP/pkg/metrics"
@ -44,7 +47,7 @@ type Runnable interface {
}
type portal struct {
env env.Core
env env.Interface
audit *logrus.Entry
log *logrus.Entry
baseAccessLog *logrus.Entry
@ -66,6 +69,7 @@ type portal struct {
dbPortal database.Portal
dbOpenShiftClusters database.OpenShiftClusters
dbSubscriptions database.Subscriptions
dialer proxy.Dialer
@ -77,7 +81,7 @@ type portal struct {
m metrics.Emitter
}
func NewPortal(env env.Core,
func NewPortal(env env.Interface,
audit *logrus.Entry,
log *logrus.Entry,
baseAccessLog *logrus.Entry,
@ -96,6 +100,7 @@ func NewPortal(env env.Core,
elevatedGroupIDs []string,
dbOpenShiftClusters database.OpenShiftClusters,
dbPortal database.Portal,
dbSubscriptions database.Subscriptions,
dialer proxy.Dialer,
m metrics.Emitter,
) Runnable {
@ -122,6 +127,7 @@ func NewPortal(env env.Core,
dbOpenShiftClusters: dbOpenShiftClusters,
dbPortal: dbPortal,
dbSubscriptions: dbSubscriptions,
dialer: dialer,
@ -304,6 +310,7 @@ func (p *portal) aadAuthenticatedRoutes(r *mux.Router, prom *prometheus.Promethe
r.Methods(http.MethodGet).Path("/api/{subscription}/{resourceGroup}/{clusterName}").HandlerFunc(p.clusterInfo)
r.Path("/api/{subscription}/{resourceGroup}/{clusterName}/nodes").HandlerFunc(p.nodes)
r.Path("/api/{subscription}/{resourceGroup}/{clusterName}/machines").HandlerFunc(p.machines)
r.Path("/api/{subscription}/{resourceGroup}/{clusterName}/vmallocationstatus").HandlerFunc(p.VMAllocationStatus)
r.Path("/api/{subscription}/{resourceGroup}/{clusterName}/machine-sets").HandlerFunc(p.machineSets)
r.Path("/api/{subscription}/{resourceGroup}/{clusterName}").HandlerFunc(p.clusterInfo)
@ -390,10 +397,39 @@ func (p *portal) makeFetcher(ctx context.Context, r *http.Request) (cluster.Fetc
} else {
dialer = p.dialer
}
return cluster.NewFetchClient(p.log, dialer, doc)
}
// makeAzureFetcher creates a cluster.AzureFetchClient suitable for use by the Portal REST API to fetch anything directly from Azure like VM Details etc.
func (p *portal) makeAzureFetcher(ctx context.Context, r *http.Request) (cluster.AzureFetchClient, error) {
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")
}
doc, err := p.dbOpenShiftClusters.Get(ctx, resourceID)
if err != nil {
return nil, err
}
subscriptionDoc, err := p.getSubscriptionDocument(ctx, doc.Key)
if err != nil {
return nil, err
}
return cluster.NewAzureFetchClient(p.log, doc, subscriptionDoc, p.env), nil
}
func (p *portal) serve(path string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
asset, err := assets.EmbeddedFiles.ReadFile(path)
@ -406,6 +442,19 @@ func (p *portal) serve(path string) func(w http.ResponseWriter, r *http.Request)
}
}
func (p *portal) getSubscriptionDocument(ctx context.Context, key string) (*api.SubscriptionDocument, error) {
r, err := azure.ParseResourceID(key)
if err != nil {
return nil, err
}
doc, err := p.dbSubscriptions.Get(ctx, r.SubscriptionID)
if cosmosdb.IsErrorStatusCode(err, http.StatusNotFound) {
return nil, api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidSubscriptionState, "", "Request is not allowed in unregistered subscription '%s'.", r.SubscriptionID)
}
return doc, err
}
func (p *portal) internalServerError(w http.ResponseWriter, err error) {
p.log.Warn(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)

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

@ -49,7 +49,7 @@ func TestSecurity(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
_env := mock_env.NewMockCore(controller)
_env := mock_env.NewMockInterface(controller)
_env.EXPECT().IsLocalDevelopmentMode().AnyTimes().Return(false)
_env.EXPECT().Location().AnyTimes().Return("eastus")
_env.EXPECT().TenantID().AnyTimes().Return("00000000-0000-0000-0000-000000000001")
@ -74,6 +74,7 @@ func TestSecurity(t *testing.T) {
dbOpenShiftClusters, _ := testdatabase.NewFakeOpenShiftClusters()
dbPortal, _ := testdatabase.NewFakePortal()
dbSubscription, _ := testdatabase.NewFakeSubscriptions()
pool := x509.NewCertPool()
pool.AddCert(servercerts[0])
@ -90,7 +91,7 @@ func TestSecurity(t *testing.T) {
},
}
p := NewPortal(_env, portalAuditLog, portalLog, portalAccessLog, l, sshl, nil, "", serverkey, servercerts, "", nil, nil, make([]byte, 32), sshkey, nil, elevatedGroupIDs, dbOpenShiftClusters, dbPortal, nil, &noop.Noop{})
p := NewPortal(_env, portalAuditLog, portalLog, portalAccessLog, l, sshl, nil, "", serverkey, servercerts, "", nil, nil, make([]byte, 32), sshkey, nil, elevatedGroupIDs, dbOpenShiftClusters, dbPortal, dbSubscription, nil, &noop.Noop{})
go func() {
err := p.Run(ctx)
if err != nil {

5
portal/v1/package-lock.json сгенерированный
Просмотреть файл

@ -6735,7 +6735,7 @@
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.1.0",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
@ -7309,8 +7309,7 @@
"dev": true
},
"minimatch": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
"version": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
"dev": true,
"requires": {

40
portal/v2/package-lock.json сгенерированный
Просмотреть файл

@ -19679,7 +19679,7 @@
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^5.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"dependencies": {
@ -19871,7 +19871,7 @@
"requires": {
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
"minimatch": "^5.1.0"
"minimatch": "^3.0.5"
}
},
"@humanwhocodes/module-importer": {
@ -20569,7 +20569,7 @@
"error-stack-parser": "^2.0.6",
"find-up": "^5.0.0",
"html-entities": "^2.1.0",
"loader-utils": "^2.0.4",
"loader-utils": "^2.0.3",
"schema-utils": "^3.0.0",
"source-map": "^0.7.3"
}
@ -21626,7 +21626,7 @@
"integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==",
"dev": true,
"requires": {
"loader-utils": "^2.0.4",
"loader-utils": "^2.0.0",
"regex-parser": "^2.2.11"
}
},
@ -21938,7 +21938,7 @@
"dev": true,
"requires": {
"find-cache-dir": "^3.3.1",
"loader-utils": "^2.0.4",
"loader-utils": "^2.0.0",
"make-dir": "^3.1.0",
"schema-utils": "^2.6.5"
},
@ -23534,7 +23534,7 @@
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash.merge": "^4.6.2",
"minimatch": "^5.1.0",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
"optionator": "^0.9.1",
"regexpp": "^3.2.0",
@ -23781,7 +23781,7 @@
"has": "^1.0.3",
"is-core-module": "^2.8.1",
"is-glob": "^4.0.3",
"minimatch": "^5.1.0",
"minimatch": "^3.1.2",
"object.values": "^1.1.5",
"resolve": "^1.22.0",
"tsconfig-paths": "^3.14.1"
@ -23839,7 +23839,7 @@
"has": "^1.0.3",
"jsx-ast-utils": "^3.3.2",
"language-tags": "^1.0.5",
"minimatch": "^5.1.0",
"minimatch": "^3.1.2",
"semver": "^6.3.0"
},
"dependencies": {
@ -23867,7 +23867,7 @@
"eslint-utils": "^3.0.0",
"ignore": "^5.1.1",
"is-core-module": "^2.11.0",
"minimatch": "^5.1.0",
"minimatch": "^3.1.2",
"resolve": "^1.22.1",
"semver": "^7.3.8"
},
@ -23903,7 +23903,7 @@
"doctrine": "^2.1.0",
"estraverse": "^5.3.0",
"jsx-ast-utils": "^2.4.1 || ^3.0.0",
"minimatch": "^5.1.0",
"minimatch": "^3.1.2",
"object.entries": "^1.1.5",
"object.fromentries": "^2.0.5",
"object.hasown": "^1.1.1",
@ -24330,7 +24330,7 @@
"integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==",
"dev": true,
"requires": {
"loader-utils": "^2.0.4",
"loader-utils": "^2.0.0",
"schema-utils": "^3.0.0"
}
},
@ -24340,7 +24340,7 @@
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
"dev": true,
"requires": {
"minimatch": "^5.1.0"
"minimatch": "^5.0.1"
}
},
"filesize": {
@ -24454,7 +24454,7 @@
"fs-extra": "^9.0.0",
"glob": "^7.1.6",
"memfs": "^3.1.2",
"minimatch": "^5.1.0",
"minimatch": "^3.0.4",
"schema-utils": "2.7.0",
"semver": "^7.3.2",
"tapable": "^1.0.0"
@ -24701,7 +24701,7 @@
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^5.1.0",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
@ -25521,7 +25521,7 @@
"async": "^3.2.3",
"chalk": "^4.0.2",
"filelist": "^1.0.1",
"minimatch": "^5.1.0"
"minimatch": "^3.0.4"
},
"dependencies": {
"ansi-styles": {
@ -29055,7 +29055,7 @@
"gzip-size": "^6.0.0",
"immer": "^9.0.7",
"is-root": "^2.1.0",
"loader-utils": "^2.0.4",
"loader-utils": "^3.2.0",
"open": "^8.4.0",
"pkg-up": "^3.1.0",
"prompts": "^2.4.2",
@ -29158,7 +29158,7 @@
"requires": {
"@babel/core": "^7.16.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
"@svgr/webpack": "^6.5.1",
"@svgr/webpack": "^5.5.0",
"babel-jest": "^27.4.2",
"babel-loader": "^8.2.3",
"babel-plugin-named-asset-import": "^0.3.8",
@ -29252,7 +29252,7 @@
"integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==",
"dev": true,
"requires": {
"minimatch": "^5.1.0"
"minimatch": "^3.0.5"
}
},
"regenerate": {
@ -29421,7 +29421,7 @@
"requires": {
"adjust-sourcemap-loader": "^4.0.0",
"convert-source-map": "^1.7.0",
"loader-utils": "^2.0.4",
"loader-utils": "^2.0.0",
"postcss": "^7.0.35",
"source-map": "0.6.1"
},
@ -30296,7 +30296,7 @@
"requires": {
"@istanbuljs/schema": "^0.1.2",
"glob": "^7.1.4",
"minimatch": "^5.1.0"
"minimatch": "^3.0.4"
}
},
"text-table": {

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

@ -53,12 +53,13 @@ export class MachinesComponent extends Component<MachinesComponentProps, IMachin
}
private extractCurrentMachine = (machineName: string): IMachine => {
let currentMachine = this.state.machines[0]
this.state.machines.forEach((machine: IMachine) => {
if (machine.name === machineName) {
return machine
currentMachine = machine
}
})
return this.state.machines[0]
return currentMachine
}
public render() {

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

@ -13,16 +13,19 @@ export declare interface IMachinesList {
name?: string;
status: string;
createdTime: string;
allocationStatus?: string;
}
interface MachinesListComponentProps {
machines: any
clusterName: string
vmAllocationStatus: Map<string, string>
}
export interface IMachinesListState {
machines: IMachine[]
clusterName: string
vmAllocationStatus: Map<string, string>
}
export class MachinesListComponent extends React.Component<MachinesListComponentProps, IMachinesListState> {
@ -33,20 +36,22 @@ export class MachinesListComponent extends React.Component<MachinesListComponent
this.state = {
machines: this.props.machines,
clusterName: this.props.clusterName,
vmAllocationStatus: this.props.vmAllocationStatus
}
}
public render() {
return (
<MachinesListHelperComponent machines={this.state.machines} clusterName={this.state.clusterName}/>
<MachinesListHelperComponent vmAllocationStatus={this.state.vmAllocationStatus} machines={this.state.machines} clusterName={this.state.clusterName}/>
)
}
}
export function MachinesListHelperComponent(props: {
machines: any,
clusterName: string
clusterName: string,
vmAllocationStatus: Map<string, string>
}) {
const [columns, setColumns] = useState<IColumn[]>([
{
@ -74,6 +79,17 @@ export function MachinesListHelperComponent(props: {
isSortedDescending: false,
showSortIconWhenUnsorted: true,
},
{
key: "allocationStatus",
name: "Allocation State",
fieldName: "allocationStatus",
minWidth: 120,
maxWidth: 120,
isResizable: true,
isSorted: true,
isSortedDescending: false,
showSortIconWhenUnsorted: true,
},
{
key: "createdTime",
name: "Created Time",
@ -97,15 +113,52 @@ export function MachinesListHelperComponent(props: {
setMachinesList(createMachinesList(props.machines))
}, [props.machines] );
// For updating machinesList with VM Allocation Status
useEffect(() => {
const initialMachineLength: number = machinesList.length
const fetchError: string = "FetchError"
if (initialMachineLength > 0) {
let localMachineList = machinesList
for (let i=0; i < localMachineList.length; i++) {
let allocationStatus: string = props.vmAllocationStatus.get(localMachineList[i].name!)!
let r: string = allocationStatus.slice(11, 12).toUpperCase() + allocationStatus.slice(12, allocationStatus.length)
localMachineList[i].allocationStatus = r
}
setMachinesList(localMachineList)
} else {
let localMachineList: IMachinesList[] = []
props.vmAllocationStatus.forEach((allocationStatus, machineName) => {
let allocationStatusShort: string = allocationStatus.slice(11, 12).toUpperCase() + allocationStatus.slice(12, allocationStatus.length)
localMachineList.push({name: machineName, status: fetchError, allocationStatus: allocationStatusShort, createdTime: fetchError,})
})
setMachinesList(localMachineList)
}
const newColumns: IColumn[] = columns.slice();
newColumns.forEach(col => {
col.onColumnClick = _onColumnClick
if (col.key == "machineName" && initialMachineLength == 0) {
col.onRender = undefined
} else if (col.key == "machineName") {
col.onRender = (item: IMachinesList) => (
<Link onClick={() => _onMachineInfoLinkClick(item.name!)}>{item.name}</Link>
)
}
})
setColumns(newColumns)
}, [props.vmAllocationStatus])
// For Shimmer
useEffect(() => {
const newColumns: IColumn[] = columns.slice();
newColumns.forEach(col => {
col.onColumnClick = _onColumnClick
})
setColumns(newColumns)
if (machinesList.length > 0) {
if (machinesList.length > 0 || props.vmAllocationStatus.keys.length > 0) {
SetShimmerVisibility(false)
}
@ -146,12 +199,11 @@ export function MachinesListHelperComponent(props: {
})
setColumns(newColumns)
//setMachinesList(machineLocal)
}
function createMachinesList(machines: IMachine[]): IMachinesList[] {
return machines.map(machine => {
return {name: machine.name, status: machine.status, createdTime: machine.createdTime}
return {name: machine.name, status: machine.status, allocationStatus: "Loading...", createdTime: machine.createdTime,}
})
}

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

@ -1,11 +1,12 @@
import { useState, useEffect, useRef } from "react"
import { AxiosResponse } from 'axios';
import { FetchMachines } from '../Request';
import { FetchMachines, FetchVMAllocationStatus } 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,
@ -23,6 +24,7 @@ export function MachinesWrapper(props: {
loaded: boolean
}) {
const [data, setData] = useState<any>([])
const [vmAllocationstatus, setVmallocationstatus] = useState<any>([])
const [error, setError] = useState<AxiosResponse | null>(null)
const state = useRef<MachinesListComponent>(null)
const [fetching, setFetching] = useState("")
@ -74,6 +76,17 @@ export function MachinesWrapper(props: {
}
}
const updateVMAllocationStatusData = (newData: any) => {
let map = new Map<string, string>()
for (var key in newData) {
map.set(key, newData[key])
}
setVmallocationstatus(map)
if (state && state.current) {
state.current.setState({ vmAllocationStatus: map })
}
}
useEffect(() => {
const onData = (result: AxiosResponse | null) => {
if (result?.status === 200) {
@ -84,20 +97,29 @@ export function MachinesWrapper(props: {
setFetching(props.currentCluster.name)
}
const onVMAllocationStatusData = (result: AxiosResponse | null) => {
if (result?.status === 200) {
updateVMAllocationStatusData(result.data)
} else {
setError(result)
}
}
if (props.detailPanelSelected.toLowerCase() == machinesKey &&
fetching === "" &&
props.loaded &&
props.currentCluster.name != "") {
setFetching("FETCHING")
FetchMachines(props.currentCluster).then(onData)
FetchVMAllocationStatus(props.currentCluster).then(onVMAllocationStatusData)
}
}, [data, props.loaded, props.detailPanelSelected])
}, [data, props.loaded, props.detailPanelSelected, vmAllocationstatus])
return (
<Stack>
<Stack.Item grow>{error && errorBar()}</Stack.Item>
<Stack>
<MachinesListComponent machines={data!} ref={state} clusterName={props.currentCluster != null ? props.currentCluster.name : ""} />
<MachinesListComponent vmAllocationStatus={vmAllocationstatus} machines={data!} ref={state} clusterName={props.currentCluster != null ? props.currentCluster.name : ""} />
</Stack>
</Stack>
)

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

@ -64,6 +64,17 @@ export const FetchMachines = async (cluster: ICluster): Promise<AxiosResponse |
}
}
export const FetchVMAllocationStatus = async (cluster: ICluster): Promise<AxiosResponse | null> => {
try {
const result = await axios(
"/api/" + cluster.subscription + "/" + cluster.resourceGroup + "/" + cluster.name + "/vmallocationstatus")
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(