Skip to content

Commit

Permalink
Add API to get detailed profile info
Browse files Browse the repository at this point in the history
To get detailed info about a Profile

```
/profile?namespace=<profile namespace>&name=<profile name>&kind=Profile
```

To get detailed info about a ClusterProfile

```
/profile?name=<clusterprofile name>&kind=ClusterProfile
```

Response will contain:

- Kind: kind of the profile (ClusterProfile vs Profile)
- Namespace: namespace of the profile (empty for ClusterProfiles)
- Name: name of the profile
- Dependencies: list of profiles the profile depends on
- Dependents: list of profiles that depend on this profile
- MatchingClusters: list of clusters (ClusterAPI or SveltosCluster) matching the profile.
this list contains *only* the clusters a user can see
- Spec: profile's spec

Here is an example:

```yaml
---
kind: "ClusterProfile"
namespace: ""
name: "prometheus-grafana"
dependencies:
- kind: "ClusterProfile"
  name: "deploy-kyverno"
  apiVersion: "config.projectsveltos.io/v1beta1"
dependents:
matchingClusters:
- kind: "Cluster"
  namespace: "default"
  name: "clusterapi-workload"
  apiVersion: "cluster.x-k8s.io/v1beta1"
spec:
  clusterSelector:
    matchLabels:
      env: "fv"
  syncMode: "Continuous"
  tier: "100"
  stopMatchingBehavior: "WithdrawPolicies"
  dependsOn:
  - "deploy-kyverno"
  helmCharts:
  - repositoryURL: "https://prometheus-community.github.io/helm-charts"
    repositoryName: "prometheus-community"
    chartName: "prometheus-community/prometheus"
    chartVersion: "23.4.0"
    releaseName: "prometheus"
    releaseNamespace: "prometheus"
    helmChartAction: "Install"
  - repositoryURL: "https://grafana.github.io/helm-charts"
    repositoryName: "grafana"
    chartName: "grafana/grafana"
    chartVersion: "6.58.9"
    releaseName: "grafana"
    releaseNamespace: "grafana"
    helmChartAction: "Install"
```
  • Loading branch information
mgianluc committed Oct 31, 2024
1 parent fe50896 commit a0d1a1d
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 22 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,28 @@ Each profile contains:
dependents: []
```
### Get a profile instance
```/profile?namespace=<profile namespace>&name=<profile name>&kind<profile kind>```

Kind can either be:
- ClusterProfile
- Profile

ClusterProfiles are cluster wide resources. Profiles are namespaced resources. So namespace is *only* required for Profile.

Response contains:

- Kind: kind of the profile (ClusterProfile vs Profile)
- Namespace: namespace of the profile (empty for ClusterProfiles)
- Name: name of the profile
- Dependencies: list of profiles the profile depends on
- Dependents: list of profiles that depend on this profile
- Spec: profile's spec
- MatchingClusters: list of clusters matching this profile. This list contains *only* the clusters users has permission for.
So if both coke and pepsi clusters are matching a profile, coke admin will only see coke clusters and pepsi admin will only
see pepsi clusters. Platform admin will see both in the response.


### How to get token

Expand Down
143 changes: 121 additions & 22 deletions internal/server/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"net/http"
"reflect"
"sort"
"strconv"
"strings"
Expand All @@ -31,8 +32,10 @@ import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"

configv1beta1 "github.com/projectsveltos/addon-controller/api/v1beta1"
libsveltosv1beta1 "github.com/projectsveltos/libsveltos/api/v1beta1"
logs "github.com/projectsveltos/libsveltos/lib/logsettings"
libsveltosset "github.com/projectsveltos/libsveltos/lib/set"
)

const (
Expand Down Expand Up @@ -321,8 +324,8 @@ var (
ginLogger.V(logs.LogDebug).Info("get managed ClusterProfiles/Profiles")

filters := getProfileFiltersFromQuery(c)
ginLogger.V(logs.LogDebug).Info(fmt.Sprintf("filters: namespace %q name %q",
filters.Namespace, filters.Name))
ginLogger.V(logs.LogDebug).Info(fmt.Sprintf("filters: kind %q namespace %q name %q",
filters.Kind, filters.Namespace, filters.Name))

user, err := validateToken(c)
if err != nil {
Expand Down Expand Up @@ -369,6 +372,95 @@ var (
// Return JSON response
c.JSON(http.StatusOK, response)
}

getProfile = func(c *gin.Context) {
ginLogger.V(logs.LogDebug).Info("get a managed ClusterProfile/Profile")

filters := getProfileFiltersFromQuery(c)
ginLogger.V(logs.LogDebug).Info(fmt.Sprintf("filters: kind %q namespace %q name %q",
filters.Kind, filters.Namespace, filters.Name))

if filters.Kind != configv1beta1.ClusterProfileKind &&
filters.Kind != configv1beta1.ProfileKind {
msg := fmt.Sprintf("supported kinds are %q and %q",
configv1beta1.ClusterProfileKind, configv1beta1.ProfileKind)
ginLogger.V(logs.LogInfo).Info(msg)
_ = c.AbortWithError(http.StatusBadRequest, errors.New(msg))
}

if filters.Kind == configv1beta1.ProfileKind {
if filters.Namespace == "" {
msg := fmt.Sprintf("namespace is required for %q", configv1beta1.ProfileKind)
ginLogger.V(logs.LogInfo).Info(msg)
_ = c.AbortWithError(http.StatusBadRequest, errors.New(msg))
}
}

if filters.Name == "" {
msg := "name is required"
ginLogger.V(logs.LogInfo).Info(msg)
_ = c.AbortWithError(http.StatusBadRequest, errors.New(msg))
}

user, err := validateToken(c)
if err != nil {
_ = c.AbortWithError(http.StatusBadRequest, err)
return
}

manager := GetManagerInstance()

var canGetResource bool
if filters.Kind == configv1beta1.ClusterProfileKind {
canGetResource, err = manager.canGetClusterProfile(filters.Name, user)
} else {
canGetResource, err = manager.canGetProfile(filters.Namespace, filters.Name, user)
}

if err != nil {
ginLogger.V(logs.LogInfo).Info(fmt.Sprintf("failed to verify permissions %s: %v", c.Request.URL, err))
_ = c.AbortWithError(http.StatusUnauthorized, err)
return
}
if !canGetResource {
ginLogger.V(logs.LogInfo).Info(fmt.Sprintf("user does not have permission to access resource. URI: %s", c.Request.URL))
_ = c.AbortWithError(http.StatusUnauthorized, err)
return
}

profileRef := &corev1.ObjectReference{
Kind: filters.Kind,
APIVersion: configv1beta1.GroupVersion.String(),
Namespace: filters.Namespace,
Name: filters.Name,
}

profileInfo := manager.GetProfile(profileRef)

if reflect.DeepEqual(profileInfo, ProfileInfo{}) {
c.JSON(http.StatusOK, "")
}

spec, matchingClusters, err := manager.getProfileSpecAndMatchingClusters(c.Request.Context(), profileRef, user)
if err != nil {
ginLogger.V(logs.LogInfo).Info(fmt.Sprintf("failed to get profile instance. %s: %v", c.Request.URL, err))
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}

result := Profile{
Kind: profileRef.Kind,
Namespace: profileRef.Namespace,
Name: profileRef.Name,
Spec: *spec,
MatchingClusters: matchingClusters,
Dependencies: transformSetToSlice(profileInfo.Dependencies),
Dependents: transformSetToSlice(profileInfo.Dependents),
}

// Return JSON response
c.JSON(http.StatusOK, result)
}
)

func (m *instance) start(ctx context.Context, port string, logger logr.Logger) {
Expand All @@ -389,6 +481,8 @@ func (m *instance) start(ctx context.Context, port string, logger logr.Logger) {
r.GET("/getClusterStatus", getClusterStatus)
// Return existing ClusterProfiles/Profiles
r.GET("/profiles", getProfiles)
// Return details about a ClusterProfile/Profile
r.GET("/profile", getProfile)

errCh := make(chan error)

Expand Down Expand Up @@ -453,6 +547,12 @@ func getProfileData(profiles map[corev1.ObjectReference]ProfileInfo, filters *pr

for k := range profiles {
profile := profiles[k]
if filters.Kind != "" {
if k.Kind != filters.Kind {
continue
}
}

if filters.Namespace != "" {
if !strings.Contains(k.Namespace, filters.Namespace) {
continue
Expand All @@ -474,26 +574,8 @@ func getProfileData(profiles map[corev1.ObjectReference]ProfileInfo, filters *pr
Kind: k.Kind,
Namespace: k.Namespace,
Name: k.Name,
Dependencies: make([]corev1.ObjectReference, profile.Dependencies.Len()),
Dependents: make([]corev1.ObjectReference, profile.Dependents.Len()),
}
dependencies := profile.Dependencies.Items()
for j := range dependencies {
tmpProfile.Dependencies[j] = corev1.ObjectReference{
Kind: k.Kind,
APIVersion: k.APIVersion,
Namespace: dependencies[j].Namespace,
Name: dependencies[j].Name,
}
}
dependents := profile.Dependents.Items()
for j := range dependents {
tmpProfile.Dependents[j] = corev1.ObjectReference{
Kind: k.Kind,
APIVersion: k.APIVersion,
Namespace: dependents[j].Namespace,
Name: dependents[j].Name,
}
Dependencies: transformSetToSlice(profile.Dependencies),
Dependents: transformSetToSlice(profile.Dependents),
}

result[profile.Tier] = append(result[profile.Tier], tmpProfile)
Expand All @@ -502,6 +584,23 @@ func getProfileData(profiles map[corev1.ObjectReference]ProfileInfo, filters *pr
return result
}

func transformSetToSlice(set *libsveltosset.Set) []corev1.ObjectReference {
result := make([]corev1.ObjectReference, set.Len())

dependencies := set.Items()

for j := range dependencies {
result[j] = corev1.ObjectReference{
Kind: dependencies[j].Kind,
APIVersion: dependencies[j].APIVersion,
Namespace: dependencies[j].Namespace,
Name: dependencies[j].Name,
}
}

return result
}

func getLimitAndSkipFromQuery(c *gin.Context) (limit, skip int) {
// Define default values for limit and skip
limit = maxItems
Expand Down
55 changes: 55 additions & 0 deletions internal/server/kubeconfig.go → internal/server/k8s-utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import (

authenticationv1 "k8s.io/api/authentication/v1"
authorizationapi "k8s.io/api/authorization/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
authenticationv1client "k8s.io/client-go/kubernetes/typed/authentication/v1"
"k8s.io/client-go/rest"
Expand All @@ -31,6 +33,7 @@ import (

configv1beta1 "github.com/projectsveltos/addon-controller/api/v1beta1"
libsveltosv1beta1 "github.com/projectsveltos/libsveltos/api/v1beta1"
"github.com/projectsveltos/libsveltos/lib/clusterproxy"
logs "github.com/projectsveltos/libsveltos/lib/logsettings"
)

Expand Down Expand Up @@ -331,3 +334,55 @@ func (m *instance) canGetProfile(profileNamespace, profileName, user string) (bo

return canI.Status.Allowed, nil
}

func (m *instance) getClusterProfileInstance(ctx context.Context, name string) (*configv1beta1.ClusterProfile, error) {
clusterProfile := configv1beta1.ClusterProfile{}

err := m.client.Get(ctx, types.NamespacedName{Name: name}, &clusterProfile)
return &clusterProfile, err
}

func (m *instance) getProfileInstance(ctx context.Context, namespace, name string) (*configv1beta1.Profile, error) {
profile := configv1beta1.Profile{}

err := m.client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: name}, &profile)
return &profile, err
}

func (m *instance) getProfileSpecAndMatchingClusters(ctx context.Context, profileRef *corev1.ObjectReference,
user string) (*configv1beta1.Spec, []corev1.ObjectReference, error) {

var spec configv1beta1.Spec
var matchingClusters []corev1.ObjectReference

if profileRef.Kind == configv1beta1.ClusterProfileKind {
cp, err := m.getClusterProfileInstance(ctx, profileRef.Name)
if err != nil {
return nil, nil, err
}

spec = cp.Spec
matchingClusters = cp.Status.MatchingClusterRefs
} else {
p, err := m.getProfileInstance(ctx, profileRef.Namespace, profileRef.Name)
if err != nil {
return nil, nil, err
}
spec = p.Spec
matchingClusters = p.Status.MatchingClusterRefs
}

accessibleMatchingClusters := make([]corev1.ObjectReference, 0)
for i := range matchingClusters {
cluster := &matchingClusters[i]
canGet, err := m.canGetCluster(cluster.Namespace, cluster.Name, user, clusterproxy.GetClusterType(cluster))
if err != nil {
return nil, nil, err
}
if canGet {
accessibleMatchingClusters = append(accessibleMatchingClusters, *cluster)
}
}

return &spec, accessibleMatchingClusters, nil
}
7 changes: 7 additions & 0 deletions internal/server/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,13 @@ func (m *instance) RemoveProfile(profile *corev1.ObjectReference) {
delete(m.profiles, *profile)
}

func (m *instance) GetProfile(profile *corev1.ObjectReference) ProfileInfo {
m.profileMux.Lock()
defer m.profileMux.Unlock()

return m.profiles[*profile]
}

// removeProfileDependency removes oldDependency from source's cached dependents
func (m *instance) removeProfileDependency(source, oldDependency *corev1.ObjectReference) {
profileInfo, ok := m.profiles[*source]
Expand Down
8 changes: 8 additions & 0 deletions internal/server/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package server
import (
"github.com/gin-gonic/gin"
corev1 "k8s.io/api/core/v1"

configv1beta1 "github.com/projectsveltos/addon-controller/api/v1beta1"
)

type Profile struct {
Expand All @@ -32,6 +34,10 @@ type Profile struct {
Dependencies []corev1.ObjectReference `json:"dependencies"`
// List of profiles that depend on this profile
Dependents []corev1.ObjectReference `json:"dependents"`
// List of managed clusters matching this profile
MatchingClusters []corev1.ObjectReference `json:"matchingClusters"`
// Profile's Spec section
Spec configv1beta1.Spec `json:"spec"`
}

type Profiles []Profile
Expand All @@ -53,13 +59,15 @@ func (s Profiles) Less(i, j int) bool {
type profileFilters struct {
Namespace string `uri:"namespace"`
Name string `uri:"name"`
Kind string `uri:"kind"`
}

func getProfileFiltersFromQuery(c *gin.Context) *profileFilters {
var filters profileFilters
// Get the values from query parameters
filters.Namespace = c.Query("namespace")
filters.Name = c.Query("name")
filters.Kind = c.Query("kind")

return &filters
}

0 comments on commit a0d1a1d

Please sign in to comment.